diff --git a/android/app/build.gradle b/android/app/build.gradle index 4e155c7..a0fbc6a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -28,11 +28,11 @@ apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' android { - compileSdkVersion 33 + compileSdkVersion 34 defaultConfig { applicationId "io.nichijou.flutter.mikan" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/assets/fonts/mono/JetBrainsMono-Bold-Italic.ttf b/assets/fonts/mono/JetBrainsMono-Bold-Italic.ttf deleted file mode 100644 index 87b9bf8..0000000 Binary files a/assets/fonts/mono/JetBrainsMono-Bold-Italic.ttf and /dev/null differ diff --git a/assets/fonts/mono/JetBrainsMono-Bold.ttf b/assets/fonts/mono/JetBrainsMono-Bold.ttf deleted file mode 100644 index fd1ab3c..0000000 Binary files a/assets/fonts/mono/JetBrainsMono-Bold.ttf and /dev/null differ diff --git a/assets/fonts/mono/JetBrainsMono-ExtraBold-Italic.ttf b/assets/fonts/mono/JetBrainsMono-ExtraBold-Italic.ttf deleted file mode 100644 index bff0884..0000000 Binary files a/assets/fonts/mono/JetBrainsMono-ExtraBold-Italic.ttf and /dev/null differ diff --git a/assets/fonts/mono/JetBrainsMono-ExtraBold.ttf b/assets/fonts/mono/JetBrainsMono-ExtraBold.ttf deleted file mode 100644 index 0e09b46..0000000 Binary files a/assets/fonts/mono/JetBrainsMono-ExtraBold.ttf and /dev/null differ diff --git a/assets/fonts/mono/JetBrainsMono-Italic.ttf b/assets/fonts/mono/JetBrainsMono-Italic.ttf deleted file mode 100644 index 2b6d374..0000000 Binary files a/assets/fonts/mono/JetBrainsMono-Italic.ttf and /dev/null differ diff --git a/assets/fonts/mono/JetBrainsMono-Medium-Italic.ttf b/assets/fonts/mono/JetBrainsMono-Medium-Italic.ttf deleted file mode 100644 index 8f7ad12..0000000 Binary files a/assets/fonts/mono/JetBrainsMono-Medium-Italic.ttf and /dev/null differ diff --git a/assets/fonts/mono/JetBrainsMono-Medium.ttf b/assets/fonts/mono/JetBrainsMono-Medium.ttf deleted file mode 100644 index f01ae48..0000000 Binary files a/assets/fonts/mono/JetBrainsMono-Medium.ttf and /dev/null differ diff --git a/assets/fonts/mono/JetBrainsMono-Regular.ttf b/assets/fonts/mono/JetBrainsMono-Regular.ttf deleted file mode 100644 index dfbece6..0000000 Binary files a/assets/fonts/mono/JetBrainsMono-Regular.ttf and /dev/null differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index ac39ce7..e784b2c 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -160,7 +160,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -385,7 +385,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 5R742PP8VF; + DEVELOPMENT_TEAM = CHR83T9WY5; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MikanProject; @@ -518,7 +518,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 5R742PP8VF; + DEVELOPMENT_TEAM = CHR83T9WY5; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MikanProject; @@ -543,7 +543,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 5R742PP8VF; + DEVELOPMENT_TEAM = CHR83T9WY5; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MikanProject; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a..a6b826d 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +26,8 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -43,9 +47,5 @@ UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index 6840c95..7388c54 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -62,8 +62,10 @@ class DefaultFirebaseOptions { projectId: 'flutter-mikan', databaseURL: 'https://flutter-mikan.firebaseio.com', storageBucket: 'flutter-mikan.appspot.com', - androidClientId: '626809778849-j0s442pnv7vs989f9jsnricrqpnu4llq.apps.googleusercontent.com', - iosClientId: '626809778849-guglnjbelfb3scmjirffr0k09uh3kgkd.apps.googleusercontent.com', + androidClientId: + '626809778849-j0s442pnv7vs989f9jsnricrqpnu4llq.apps.googleusercontent.com', + iosClientId: + '626809778849-guglnjbelfb3scmjirffr0k09uh3kgkd.apps.googleusercontent.com', iosBundleId: 'io.nichijou.flutter.mikan', ); @@ -74,8 +76,10 @@ class DefaultFirebaseOptions { projectId: 'flutter-mikan', databaseURL: 'https://flutter-mikan.firebaseio.com', storageBucket: 'flutter-mikan.appspot.com', - androidClientId: '626809778849-j0s442pnv7vs989f9jsnricrqpnu4llq.apps.googleusercontent.com', - iosClientId: '626809778849-ob4j2tt7qv9kis69servg1m464um9b9c.apps.googleusercontent.com', + androidClientId: + '626809778849-j0s442pnv7vs989f9jsnricrqpnu4llq.apps.googleusercontent.com', + iosClientId: + '626809778849-ob4j2tt7qv9kis69servg1m464um9b9c.apps.googleusercontent.com', iosBundleId: 'io.nichijou.flutter.mikan.RunnerTests', ); } diff --git a/lib/internal/extension.dart b/lib/internal/extension.dart index 8c08637..77c8345 100644 --- a/lib/internal/extension.dart +++ b/lib/internal/extension.dart @@ -7,12 +7,10 @@ import 'package:android_intent_plus/flag.dart'; import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:oktoast/oktoast.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import '../topvars.dart'; - extension IterableExt on Iterable? { bool get isNullOrEmpty => this == null || this!.isEmpty; @@ -109,37 +107,9 @@ extension NullableStringExt on String? { if (isNullOrBlank) { return; } - showToastWidget( - Material( - color: Colors.transparent, - child: Builder( - builder: (context) { - final theme = Theme.of(context); - return Stack( - alignment: AlignmentDirectional.center, - children: [ - Container( - padding: edgeH16V8, - margin: edgeH24, - decoration: BoxDecoration( - color: theme.secondary, - borderRadius: borderRadius28, - ), - child: Text( - this!.trim(), - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyMedium?.copyWith( - color: - theme.secondary.isDark ? Colors.white : Colors.black, - ), - ), - ), - ], - ); - }, - ), - ), + SmartDialog.showToast( + this!, + alignment: const AlignmentDirectional(0.0, 0.72), ); HapticFeedback.mediumImpact(); } @@ -163,7 +133,7 @@ extension NullableStringExt on String? { action: 'android.intent.action.VIEW', flags: [ Flag.FLAG_ACTIVITY_NEW_TASK, - Flag.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + Flag.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED, ], data: this, ).launch().catchError((e, s) async { @@ -180,7 +150,7 @@ extension NullableStringExt on String? { if (isNullOrBlank) { return '内容为空,取消操作'.toast(); } - FlutterClipboard.copy(this!).then((_) => '成功复制到剪切板'.toast()); + FlutterClipboard.copy(this!).then((_) => '已复制到剪切板'.toast()); } void share() { diff --git a/lib/internal/http.dart b/lib/internal/http.dart index 82adabb..1f5aa36 100644 --- a/lib/internal/http.dart +++ b/lib/internal/http.dart @@ -39,9 +39,9 @@ class MikanTransformer extends SyncTransformer { @override Future transformResponse( RequestOptions options, - ResponseBody response, + ResponseBody responseBody, ) async { - final rep = await super.transformResponse(options, response); + final rep = await super.transformResponse(options, responseBody); if (rep is String) { final String? func = options.extra['$MikanFunc']; if (func.isNotBlank) { @@ -162,7 +162,7 @@ class Http { } } catch (e, s) { e.$error(stackTrace: s); - if (e is DioError) { + if (e is DioException) { if (e.response?.statusCode == 302 && options.method == _InnerMethod.form && (e.requestOptions.path == MikanUrls.login || diff --git a/lib/internal/http_cache_manager.dart b/lib/internal/http_cache_manager.dart index bee7bb0..766a3c7 100644 --- a/lib/internal/http_cache_manager.dart +++ b/lib/internal/http_cache_manager.dart @@ -62,7 +62,7 @@ class HttpCacheManager { _tasks.remove(url); throw value; }), - completer.future + completer.future, ]); } diff --git a/lib/internal/lifecycle.dart b/lib/internal/lifecycle.dart index 210d9a5..535d735 100644 --- a/lib/internal/lifecycle.dart +++ b/lib/internal/lifecycle.dart @@ -26,6 +26,7 @@ abstract class LifecycleState extends State case AppLifecycleState.inactive: case AppLifecycleState.paused: case AppLifecycleState.detached: + case AppLifecycleState.hidden: onPause(); } } @@ -137,6 +138,7 @@ abstract class LifecycleAppState extends State case AppLifecycleState.inactive: case AppLifecycleState.paused: case AppLifecycleState.detached: + case AppLifecycleState.hidden: onPause(); } } diff --git a/lib/internal/log.dart b/lib/internal/log.dart index 2245e69..84e6b93 100644 --- a/lib/internal/log.dart +++ b/lib/internal/log.dart @@ -48,7 +48,7 @@ extension Log on Object? { StackTrace? stackTrace, int level = 1, }) { - if(!kDebugMode){ + if (!kDebugMode) { return; } final track = tag ?? _trackStackTraceId(StackTrace.current, level); @@ -67,7 +67,7 @@ extension Log on Object? { StackTrace? stackTrace, int level = 1, }) { - if(!kDebugMode){ + if (!kDebugMode) { return; } final track = tag ?? _trackStackTraceId(StackTrace.current, level); @@ -86,7 +86,7 @@ extension Log on Object? { StackTrace? stackTrace, int level = 1, }) { - if(!kDebugMode){ + if (!kDebugMode) { return; } final track = tag ?? _trackStackTraceId(StackTrace.current, level); @@ -106,7 +106,7 @@ extension Log on Object? { StackTrace? stackTrace, int level = 1, }) { - if(!kDebugMode){ + if (!kDebugMode) { return; } final track = tag ?? _trackStackTraceId(StackTrace.current, level); @@ -191,7 +191,6 @@ extension Log on Object? { return '${now.hour.toString().padLeft(2, '0')}' ':${now.minute.toString().padLeft(2, '0')}' ':${now.second.toString().padLeft(2, '0')}' - '.${now.millisecond.toString().padLeft(3, '0')}' - ; + '.${now.millisecond.toString().padLeft(3, '0')}'; } } diff --git a/lib/internal/method.dart b/lib/internal/method.dart new file mode 100644 index 0000000..f9ebe20 --- /dev/null +++ b/lib/internal/method.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; + +import '../topvars.dart'; + +Future wrapLoading( + FutureOr Function() block, { + String msg = '加载中...', +}) async { + try { + unawaited( + SmartDialog.showLoading( + backDismiss: false, + clickMaskDismiss: false, + maskColor: Theme.of(navKey.currentContext!) + .colorScheme + .background + .withOpacity(0.64), + msg: msg, + ), + ); + return await block(); + } finally { + await SmartDialog.dismiss(); + } +} + +Future hideKeyboard() { + return SystemChannels.textInput + .invokeMethod('TextInput.hide') + .catchError((_) {}); +} diff --git a/lib/internal/repo.dart b/lib/internal/repo.dart index 00c88c7..0990264 100644 --- a/lib/internal/repo.dart +++ b/lib/internal/repo.dart @@ -105,8 +105,10 @@ class Repo { int bangumiId, bool subscribe, { int? subgroupId, + // 1: 简中,2: 繁中 + int? language, }) { - final Options options = Options( + final options = Options( contentType: Headers.jsonContentType, responseType: ResponseType.json, ); @@ -115,6 +117,7 @@ class Repo { data: { 'BangumiID': bangumiId, 'SubtitleGroupID': subgroupId, + if (language != null) 'Language': language, }, options: options, ); diff --git a/lib/internal/resolver.dart b/lib/internal/resolver.dart index 97a32a0..a04b33b 100644 --- a/lib/internal/resolver.dart +++ b/lib/internal/resolver.dart @@ -187,10 +187,15 @@ class Resolver { .querySelector('#login input[name=__RequestVerificationToken]') ?.attributes['value'] ?.trim(); + final rss = document + .querySelector('#an-episode-updates .mikan-rss') + ?.attributes['href'] + ?.trim(); return User( name: name, avatar: avatar == null ? null : MikanUrls.baseUrl + avatar, token: token, + rss: rss == null ? null : MikanUrls.baseUrl + rss, ); } @@ -603,8 +608,23 @@ class Resolver { } else { subgroupBangumi.name = temp!; } - subgroupBangumi.subscribed = - element.querySelector('.subscribed')?.text.trim() == '已订阅'; + final subele = element.querySelector('.subscribed')!; + subgroupBangumi.subscribed = !subele.attributes.containsKey('style'); + subgroupBangumi.sublang = subele.text; + if (subgroupBangumi.subscribed) { + if (subgroupBangumi.sublang == '简中') { + subgroupBangumi.state = 1; + } else if (subgroupBangumi.sublang == '繁中') { + subgroupBangumi.state = 2; + } else { + subgroupBangumi.state = 0; + } + } else { + subgroupBangumi.state = -1; + } + final rss = + element.querySelector('.mikan-rss')?.attributes['href']?.trim(); + subgroupBangumi.rss = rss == null ? null : MikanUrls.baseUrl + rss; subgroups = []; elements = element.querySelectorAll('ul > li > a'); if (elements.isSafeNotEmpty) { diff --git a/lib/main.dart b/lib/main.dart index 1ca65a0..1056041 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,8 +11,8 @@ import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:oktoast/oktoast.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; @@ -20,9 +20,9 @@ import 'internal/dynamic_color.dart'; import 'internal/extension.dart'; import 'internal/hive.dart'; import 'internal/http_cache_manager.dart'; -import 'internal/kit.dart'; import 'internal/lifecycle.dart'; import 'internal/log.dart'; +import 'internal/method.dart'; import 'internal/network_font_loader.dart'; import 'mikan_route.dart'; import 'mikan_routes.dart'; @@ -33,7 +33,9 @@ import 'providers/list_model.dart'; import 'providers/op_model.dart'; import 'providers/subscribed_model.dart'; import 'topvars.dart'; +import 'widget/loading.dart'; import 'widget/restart.dart'; +import 'widget/toast.dart'; final _analytics = FirebaseAnalytics.instance; final _observer = FirebaseAnalyticsObserver(analytics: _analytics); @@ -165,6 +167,7 @@ class _MikanAppState extends State { builder: (mode, lightColorScheme, darkColorScheme, fontFamily) { final navigatorObservers = [ Lifecycle.lifecycleRouteObserver, + FlutterSmartDialog.observer, if (isSupportFirebase) _observer, FFNavigatorObserver( routeChange: (newRoute, oldRoute) { @@ -192,23 +195,24 @@ class _MikanAppState extends State { brightness: Brightness.light, fontFamily: fontFamily, colorScheme: lightColorScheme, + visualDensity: VisualDensity.standard, ), darkTheme: ThemeData( useMaterial3: true, brightness: Brightness.dark, fontFamily: fontFamily, colorScheme: darkColorScheme, + visualDensity: VisualDensity.standard, ), initialRoute: Routes.splash.name, - builder: (context, child) { - return OKToast( - position: ToastPosition( - align: Alignment.bottomCenter, - offset: -context.screenHeight * 0.18, - ), - child: child!, - ); - }, + builder: FlutterSmartDialog.init( + toastBuilder: (msg) => ToastWidget(msg: msg), + loadingBuilder: (msg) => LoadingWidget(msg: msg), + builder: (context, child) => GestureDetector( + onTap: hideKeyboard, + child: child, + ), + ), onGenerateRoute: (RouteSettings settings) { return onGenerateRoute( settings: settings, diff --git a/lib/mikan_route.dart b/lib/mikan_route.dart index 9bd2b03..9011fb1 100644 --- a/lib/mikan_route.dart +++ b/lib/mikan_route.dart @@ -180,7 +180,7 @@ FFRouteSettings getRouteSettings({ return FFRouteSettings( name: name, arguments: arguments, - builder: () => SearchFragment( + builder: () => Search( key: asT( safeArguments['key'], ), diff --git a/lib/model/announcement.dart b/lib/model/announcement.dart index 1fa1594..0f2fdba 100644 --- a/lib/model/announcement.dart +++ b/lib/model/announcement.dart @@ -43,8 +43,6 @@ class Announcement extends HiveObject { } } - - @HiveType(typeId: MyHive.mikanAnnouncementNode) class AnnouncementNode extends HiveObject { AnnouncementNode({ diff --git a/lib/model/announcement.g.dart b/lib/model/announcement.g.dart index c51db25..d127730 100644 --- a/lib/model/announcement.g.dart +++ b/lib/model/announcement.g.dart @@ -54,18 +54,21 @@ class AnnouncementNodeAdapter extends TypeAdapter { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return AnnouncementNode( - place: fields[3] as String?, text: fields[0] as String, + type: fields[1] as String?, + place: fields[2] as String?, ); } @override void write(BinaryWriter writer, AnnouncementNode obj) { writer - ..writeByte(2) + ..writeByte(3) ..writeByte(0) ..write(obj.text) - ..writeByte(3) + ..writeByte(1) + ..write(obj.type) + ..writeByte(2) ..write(obj.place); } diff --git a/lib/model/carousel.dart b/lib/model/carousel.dart index 11988bd..ff53820 100644 --- a/lib/model/carousel.dart +++ b/lib/model/carousel.dart @@ -1,4 +1,5 @@ import 'package:hive/hive.dart'; + import '../internal/hive.dart'; part 'carousel.g.dart'; diff --git a/lib/model/subgroup_bangumi.dart b/lib/model/subgroup_bangumi.dart index 75bd017..d254d34 100644 --- a/lib/model/subgroup_bangumi.dart +++ b/lib/model/subgroup_bangumi.dart @@ -6,6 +6,9 @@ class SubgroupBangumi { late String dataId; late List subgroups; late bool subscribed; + String? sublang; + String? rss; + late int state; late List records; @override @@ -17,6 +20,9 @@ class SubgroupBangumi { dataId == other.dataId && subgroups == other.subgroups && subscribed == other.subscribed && + sublang == other.sublang && + rss == other.rss && + state == other.state && records == other.records; @override @@ -25,5 +31,8 @@ class SubgroupBangumi { dataId.hashCode ^ subgroups.hashCode ^ subscribed.hashCode ^ + sublang.hashCode ^ + rss.hashCode ^ + state.hashCode ^ records.hashCode; } diff --git a/lib/model/user.dart b/lib/model/user.dart index 413beba..ae595dd 100644 --- a/lib/model/user.dart +++ b/lib/model/user.dart @@ -7,12 +7,13 @@ part 'user.g.dart'; @HiveType(typeId: MyHive.mikanUser) class User extends HiveObject { - User({ this.name, this.avatar, this.token, + this.rss, }); + @HiveField(0) String? name; @@ -22,6 +23,9 @@ class User extends HiveObject { @HiveField(2) String? token; + @HiveField(3) + String? rss; + bool get hasLogin => name.isNotBlank && avatar.isNotBlank; @override @@ -31,8 +35,10 @@ class User extends HiveObject { runtimeType == other.runtimeType && name == other.name && avatar == other.avatar && - token == other.token; + token == other.token && + rss == other.rss; @override - int get hashCode => name.hashCode ^ avatar.hashCode ^ token.hashCode; + int get hashCode => + name.hashCode ^ avatar.hashCode ^ token.hashCode ^ rss.hashCode; } diff --git a/lib/model/user.g.dart b/lib/model/user.g.dart index 6edd5ec..be6eb7c 100644 --- a/lib/model/user.g.dart +++ b/lib/model/user.g.dart @@ -20,19 +20,22 @@ class UserAdapter extends TypeAdapter { name: fields[0] as String?, avatar: fields[1] as String?, token: fields[2] as String?, + rss: fields[3] as String?, ); } @override void write(BinaryWriter writer, User obj) { writer - ..writeByte(3) + ..writeByte(4) ..writeByte(0) ..write(obj.name) ..writeByte(1) ..write(obj.avatar) ..writeByte(2) - ..write(obj.token); + ..write(obj.token) + ..writeByte(3) + ..write(obj.rss); } @override diff --git a/lib/providers/bangumi_model.dart b/lib/providers/bangumi_model.dart index 08a1fc2..6df2c28 100644 --- a/lib/providers/bangumi_model.dart +++ b/lib/providers/bangumi_model.dart @@ -5,7 +5,6 @@ import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import '../internal/extension.dart'; -import '../internal/http.dart'; import '../internal/repo.dart'; import '../model/bangumi_details.dart'; import 'base_model.dart'; @@ -76,15 +75,14 @@ class BangumiModel extends BaseModel { if (bid == null) { return '番组id为空,忽略当前订阅'.toast(); } - final Resp resp = await Repo.subscribeBangumi( + final resp = await Repo.subscribeBangumi( bid, _bangumiDetail!.subscribed, ); if (resp.success) { - _bangumiDetail!.subscribed = !_bangumiDetail!.subscribed; + await load(); } else { return resp.msg.toast(); } - notifyListeners(); } } diff --git a/lib/providers/forgot_password_model.dart b/lib/providers/forgot_password_model.dart index 51d7e52..ff09fb0 100644 --- a/lib/providers/forgot_password_model.dart +++ b/lib/providers/forgot_password_model.dart @@ -31,7 +31,7 @@ class ForgotPasswordModel extends BaseModel { } final Map params = { 'Email': _emailController.text, - '__RequestVerificationToken': token + '__RequestVerificationToken': token, }; final Resp resp = await Repo.forgotPassword(params); _loading = false; diff --git a/lib/providers/home_model.dart b/lib/providers/home_model.dart index 714c7d0..75b7aa6 100644 --- a/lib/providers/home_model.dart +++ b/lib/providers/home_model.dart @@ -9,6 +9,7 @@ import '../internal/hive.dart'; import '../internal/http.dart'; import '../internal/log.dart'; import '../internal/repo.dart'; +import '../res/assets.gen.dart'; import '../topvars.dart'; import '../widget/bottom_sheet.dart'; import 'base_model.dart'; @@ -95,11 +96,7 @@ class HomeModel extends BaseModel { children: [ Row( children: [ - Image.asset( - 'assets/mikan.png', - height: 42.0, - width: 42.0, - ), + Assets.mikan.image(width: 42.0), sizedBoxW12, Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -133,7 +130,7 @@ class HomeModel extends BaseModel { maxLines: 1, ), ], - ) + ), ], ), ], @@ -202,7 +199,7 @@ class HomeModel extends BaseModel { 'armeabi-v7a', 'x86_64', 'universal', - 'win32' + 'win32', }.firstWhere( (arch) => item['name'].contains(arch), orElse: () => null, diff --git a/lib/providers/login_model.dart b/lib/providers/login_model.dart index 9356a83..0734234 100644 --- a/lib/providers/login_model.dart +++ b/lib/providers/login_model.dart @@ -48,7 +48,7 @@ class LoginModel extends BaseModel { Future submit(VoidCallback loginSuccess) async { _loading = true; notifyListeners(); - final Resp tokenResp = await Repo.refreshLoginToken(); + final Resp tokenResp = await Repo.refreshLoginToken(); if (!tokenResp.success) { _loading = false; notifyListeners(); @@ -64,9 +64,9 @@ class LoginModel extends BaseModel { 'UserName': _accountController.text, 'Password': _passwordController.text, 'RememberMe': _rememberMe, - '__RequestVerificationToken': token + '__RequestVerificationToken': token, }; - final Resp resp = await Repo.login(loginParams); + final Resp resp = await Repo.login(loginParams); _loading = false; notifyListeners(); if (resp.success) { diff --git a/lib/providers/register_model.dart b/lib/providers/register_model.dart index f5249b7..c03d29f 100644 --- a/lib/providers/register_model.dart +++ b/lib/providers/register_model.dart @@ -58,7 +58,7 @@ class RegisterModel extends BaseModel { 'ConfirmPassword': _confirmPasswordController.text, 'Email': _emailController.text, 'QQ': _qqController.text, - '__RequestVerificationToken': token + '__RequestVerificationToken': token, }; final Resp resp = await Repo.register(registerParams); _loading = false; diff --git a/lib/providers/subscribed_model.dart b/lib/providers/subscribed_model.dart index 25f8d37..4b0f2f1 100644 --- a/lib/providers/subscribed_model.dart +++ b/lib/providers/subscribed_model.dart @@ -14,8 +14,6 @@ import 'base_model.dart'; class SubscribedModel extends BaseModel { SubscribedModel(); - bool _seasonLoading = true; - bool _recordsLoading = true; Season? _season; List? _bangumis; Map>? _rss; @@ -36,10 +34,6 @@ class SubscribedModel extends BaseModel { List? get records => _records; - bool get seasonLoading => _seasonLoading; - - bool get recordsLoading => _recordsLoading; - Season? get season => _season; List? get bangumis => _bangumis; @@ -53,18 +47,10 @@ class SubscribedModel extends BaseModel { final completer = Completer(); _completer = completer; Future(() { - _seasonLoading = true; - _recordsLoading = true; return Future.wait( [ - _loadRecentRecords().whenComplete(() { - _recordsLoading = false; - notifyListeners(); - }), - _loadMySubscribedSeasonBangumi(_season).whenComplete(() { - _seasonLoading = false; - notifyListeners(); - }) + _loadRecentRecords(), + _loadMySubscribedSeasonBangumi(_season), ], ) .then((value) => IndicatorResult.success) @@ -81,9 +67,11 @@ class SubscribedModel extends BaseModel { return; } _season = season; - final resp = await Repo.mySubscribedSeasonBangumi(season.year, season.season); + final resp = + await Repo.mySubscribedSeasonBangumi(season.year, season.season); if (resp.success) { _bangumis = resp.data; + notifyListeners(); } else { '获取季度订阅失败 ${resp.msg ?? ''}'.toast(); } @@ -94,6 +82,7 @@ class SubscribedModel extends BaseModel { if (resp.success) { _records = resp.data ?? []; _rss = groupBy(resp.data ?? [], (it) => it.id!); + notifyListeners(); } else { '获取最近更新失败 ${resp.msg ?? ''}'.toast(); } diff --git a/lib/res/assets.gen.dart b/lib/res/assets.gen.dart new file mode 100644 index 0000000..2b58ac1 --- /dev/null +++ b/lib/res/assets.gen.dart @@ -0,0 +1,93 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use + +import 'package:flutter/widgets.dart'; + +class Assets { + Assets._(); + + static const AssetGenImage mikan = AssetGenImage('assets/mikan.png'); + + /// List of all assets + List get values => [mikan]; +} + +class AssetGenImage { + const AssetGenImage(this._assetName); + + final String _assetName; + + Image image({ + Key? key, + AssetBundle? bundle, + ImageFrameBuilder? frameBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? scale, + double? width, + double? height, + Color? color, + Animation? opacity, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + String? package, + FilterQuality filterQuality = FilterQuality.low, + int? cacheWidth, + int? cacheHeight, + }) { + return Image.asset( + _assetName, + key: key, + bundle: bundle, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + scale: scale, + width: width, + height: height, + color: color, + opacity: opacity, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + package: package, + filterQuality: filterQuality, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, + ); + } + + ImageProvider provider({ + AssetBundle? bundle, + String? package, + }) { + return AssetImage( + _assetName, + bundle: bundle, + package: package, + ); + } + + String get path => _assetName; + + String get keyName => _assetName; +} diff --git a/lib/ui/components/rss_record_item.dart b/lib/ui/components/rss_record_item.dart index fca7672..d61c38d 100644 --- a/lib/ui/components/rss_record_item.dart +++ b/lib/ui/components/rss_record_item.dart @@ -163,7 +163,7 @@ class RssRecordItem extends StatelessWidget { ), ], ), - ) + ), ], ), ); diff --git a/lib/ui/fragments/bangumi_cover_scroll_list.dart b/lib/ui/fragments/bangumi_cover_scroll_list.dart index 120a4ba..5544321 100644 --- a/lib/ui/fragments/bangumi_cover_scroll_list.dart +++ b/lib/ui/fragments/bangumi_cover_scroll_list.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:waterfall_flow/waterfall_flow.dart'; import '../../internal/image_provider.dart'; import '../../model/bangumi_row.dart'; @@ -65,24 +66,41 @@ class _BangumiCoverScrollListFragmentState } Widget _buildList(ThemeData theme, List bangumiRows) { - final bangumis = - bangumiRows.map((e) => e.bangumis).expand((element) => element); + final bangumis = bangumiRows + .map((e) => e.bangumis) + .expand((e) => e) + .toList(growable: false); final length = bangumis.length; if (length == 0) { return sizedBox; } - return GridView.builder( + return WaterfallFlow.builder( controller: _scrollController, physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 400.0, - childAspectRatio: 0.75, + gridDelegate: const SliverWaterfallFlowDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 320.0, + crossAxisSpacing: 24.0, + mainAxisSpacing: 24.0, ), + padding: const EdgeInsets.all(24.0), itemBuilder: (_, index) { - final bangumi = bangumis.elementAt(index % length); - return Image( - image: CacheImage(bangumi.cover), - fit: BoxFit.cover, + final bangumi = bangumis[index % length]; + return ClipRRect( + borderRadius: borderRadius12, + child: Image( + image: CacheImage(bangumi.cover), + fit: BoxFit.cover, + loadingBuilder: ( + context, + child, + loadingProgress, + ) { + if (loadingProgress == null) { + return child; + } + return const AspectRatio(aspectRatio: 1.0); + }, + ), ); }, ); diff --git a/lib/ui/fragments/card_style.dart b/lib/ui/fragments/card_style.dart index 35f5a8c..c67f85f 100644 --- a/lib/ui/fragments/card_style.dart +++ b/lib/ui/fragments/card_style.dart @@ -35,9 +35,16 @@ class CardStyle extends StatelessWidget { label: Text('样式$v'), ); }), - onSelectionChanged: (v){ + onSelectionChanged: (v) { MyHive.setCardStyle(v.first); }, + style: ButtonStyle( + shape: MaterialStateProperty.resolveWith((states) { + return const RoundedRectangleBorder( + borderRadius: borderRadius12, + ); + }), + ), selected: {value}, ); }, diff --git a/lib/ui/fragments/forgot_password_confirm.dart b/lib/ui/fragments/forgot_password_confirm.dart index a6a379e..36aaf74 100644 --- a/lib/ui/fragments/forgot_password_confirm.dart +++ b/lib/ui/fragments/forgot_password_confirm.dart @@ -27,7 +27,7 @@ class ForgotPasswordConfirm extends StatelessWidget { ], ), ), - ) + ), ], ), ); diff --git a/lib/ui/fragments/index.dart b/lib/ui/fragments/index.dart index 359670b..03dd2e3 100644 --- a/lib/ui/fragments/index.dart +++ b/lib/ui/fragments/index.dart @@ -3,6 +3,7 @@ import 'dart:math' as math; import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; +import 'package:infinite_carousel/infinite_carousel.dart'; import 'package:provider/provider.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:waterfall_flow/waterfall_flow.dart'; @@ -11,7 +12,6 @@ import '../../internal/delegate.dart'; import '../../internal/extension.dart'; import '../../internal/image_provider.dart'; import '../../internal/kit.dart'; -import '../../internal/lifecycle.dart'; import '../../mikan_routes.dart'; import '../../model/bangumi_row.dart'; import '../../model/carousel.dart'; @@ -22,15 +22,14 @@ import '../../providers/index_model.dart'; import '../../providers/op_model.dart'; import '../../topvars.dart'; import '../../widget/bottom_sheet.dart'; -import '../../widget/infinite_carousel.dart'; import '../../widget/ripple_tap.dart'; import '../../widget/scalable_tap.dart'; import '../../widget/sliver_pinned_header.dart'; import '../components/ova_record_item.dart'; -import 'bangumi_sliver_grid.dart'; import 'select_season.dart'; import 'select_tablet_mode.dart'; import 'settings.dart'; +import 'sliver_bangumi_list.dart'; class IndexFragment extends StatefulWidget { const IndexFragment({super.key}); @@ -39,7 +38,7 @@ class IndexFragment extends StatefulWidget { State createState() => _IndexFragmentState(); } -class _IndexFragmentState extends LifecycleAppState { +class _IndexFragmentState extends State { final _infiniteScrollController = InfiniteScrollController(); Timer? _timer; @@ -63,13 +62,6 @@ class _IndexFragmentState extends LifecycleAppState { super.dispose(); } - @override - void onResume() { - if (mounted) { - Provider.of(context, listen: false).refresh(); - } - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -96,7 +88,7 @@ class _IndexFragmentState extends LifecycleAppState { pushPinnedChildren: true, children: [ _buildWeekSection(theme, bangumiRow), - BangumiSliverGridFragment( + SliverBangumiList( bangumis: bangumiRow.bangumis, handleSubscribe: (bangumi, flag) { context.read().subscribeBangumi( @@ -138,14 +130,14 @@ class _IndexFragmentState extends LifecycleAppState { if (bangumiRow.subscribedUpdatedNum > 0) '💖 ${bangumiRow.subscribedUpdatedNum}部', if (bangumiRow.subscribedNum > 0) '❤ ${bangumiRow.subscribedNum}部', - '🎬 ${bangumiRow.num}部' + '🎬 ${bangumiRow.num}部', ].join(','); final full = [ if (bangumiRow.updatedNum > 0) '更新${bangumiRow.updatedNum}部', if (bangumiRow.subscribedUpdatedNum > 0) '订阅更新${bangumiRow.subscribedUpdatedNum}部', if (bangumiRow.subscribedNum > 0) '订阅${bangumiRow.subscribedNum}部', - '共${bangumiRow.num}部' + '共${bangumiRow.num}部', ].join(','); return SliverPinnedHeader( @@ -189,7 +181,7 @@ class _IndexFragmentState extends LifecycleAppState { if (carousels.isNotEmpty) { return SliverToBoxAdapter( child: SizedBox( - height: 180.0, + height: 160.0, child: InfiniteCarousel.builder( itemBuilder: (context, index, realIndex) { final carousel = carousels[index]; @@ -479,6 +471,15 @@ class _PinedHeader extends StatelessWidget { child: Container( decoration: BoxDecoration( color: theme.colorScheme.background, + border: offsetRatio > 0.1 + ? Border( + bottom: Divider.createBorderSide( + context, + color: theme.colorScheme.outlineVariant, + width: 0.0, + ), + ) + : null, ), padding: EdgeInsetsDirectional.only( start: 12.0, diff --git a/lib/ui/fragments/list.dart b/lib/ui/fragments/list.dart index a337847..40a8a95 100644 --- a/lib/ui/fragments/list.dart +++ b/lib/ui/fragments/list.dart @@ -101,7 +101,7 @@ class _PinedHeader extends StatelessWidget { showSearchPanel(context); }, icon: const Icon(Icons.search_rounded), - ) + ), ], ); }, diff --git a/lib/ui/fragments/select_season.dart b/lib/ui/fragments/select_season.dart index 67d2ce1..d6f8ee9 100644 --- a/lib/ui/fragments/select_season.dart +++ b/lib/ui/fragments/select_season.dart @@ -69,21 +69,23 @@ class SelectSeasonFragment extends StatelessWidget { return Tooltip( message: season.title, child: RippleTap( - color: selected ? theme.primary : theme.secondary, + color: selected + ? theme.primary.withOpacity(0.38) + : theme.secondary.withOpacity(0.1), borderRadius: borderRadius8, onTap: () { Navigator.pop(context); indexModel.selectSeason(season); }, child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), child: Text( season.season, - style: theme.textTheme.bodyLarge!.copyWith( - color: selected - ? theme.colorScheme.onPrimary - : theme.colorScheme.onSecondary, - ), + textAlign: TextAlign.center, + style: theme.textTheme.labelLarge, ), ), ), @@ -120,7 +122,7 @@ class SelectSeasonFragment extends StatelessWidget { width: 78.0, child: Text( year.year, - style: theme.textTheme.headlineSmall, + style: theme.textTheme.titleLarge, ), ), sizedBoxW12, diff --git a/lib/ui/fragments/settings.dart b/lib/ui/fragments/settings.dart index 40ea942..2efcd7b 100644 --- a/lib/ui/fragments/settings.dart +++ b/lib/ui/fragments/settings.dart @@ -12,6 +12,7 @@ import '../../model/user.dart'; import '../../providers/home_model.dart'; import '../../providers/index_model.dart'; import '../../providers/settings_model.dart'; +import '../../res/assets.gen.dart'; import '../../topvars.dart'; import '../../widget/bottom_sheet.dart'; import '../../widget/ripple_tap.dart'; @@ -134,7 +135,7 @@ class SettingsPanel extends StatelessWidget { style: theme.textTheme.bodyMedium, ); }, - ) + ), ], ), ), @@ -170,7 +171,7 @@ class SettingsPanel extends StatelessWidget { style: theme.textTheme.bodyMedium, ); }, - ) + ), ], ), ), @@ -210,7 +211,7 @@ class SettingsPanel extends StatelessWidget { style: theme.textTheme.bodyMedium, ); }, - ) + ), ], ), ), @@ -250,7 +251,7 @@ class SettingsPanel extends StatelessWidget { style: theme.textTheme.bodyMedium, ); }, - ) + ), ], ), ), @@ -290,7 +291,7 @@ class SettingsPanel extends StatelessWidget { style: theme.textTheme.bodyMedium, ); }, - ) + ), ], ), ), @@ -325,7 +326,7 @@ class SettingsPanel extends StatelessWidget { style: theme.textTheme.bodyMedium, ); }, - ) + ), ], ), ), @@ -390,7 +391,7 @@ class SettingsPanel extends StatelessWidget { ); }, ), - ) + ), ], ), ); @@ -571,7 +572,7 @@ class SettingsPanel extends StatelessWidget { }); }, child: const Text('确定'), - ) + ), ], ); }, @@ -613,7 +614,13 @@ class SettingsPanel extends StatelessWidget { return Transform.translate( offset: const Offset(8.0, 0.0), child: IconButton( - onPressed: () {}, + onPressed: () { + MBottomSheet.show( + context, + (context) => + const MBottomSheet(child: ThemeColorPanel()), + ); + }, icon: Icon( Icons.circle_rounded, color: Color(colorSeed), @@ -638,27 +645,13 @@ Widget buildAvatar(String? avatar) { width: 36.0, height: 36.0, loadingBuilder: (_, child, event) { - return event == null - ? child - : Image.asset( - 'assets/mikan.png', - width: 36.0, - height: 36.0, - ); + return event == null ? child : Assets.mikan.image(width: 36.0); }, errorBuilder: (_, __, ___) { - return Image.asset( - 'assets/mikan.png', - width: 36.0, - height: 36.0, - ); + return Assets.mikan.image(width: 36.0); }, ) - : Image.asset( - 'assets/mikan.png', - width: 36.0, - height: 36.0, - ), + : Assets.mikan.image(width: 36.0), ); } diff --git a/lib/ui/fragments/bangumi_sliver_grid.dart b/lib/ui/fragments/sliver_bangumi_list.dart similarity index 56% rename from lib/ui/fragments/bangumi_sliver_grid.dart rename to lib/ui/fragments/sliver_bangumi_list.dart index 3ae9829..817b7da 100644 --- a/lib/ui/fragments/bangumi_sliver_grid.dart +++ b/lib/ui/fragments/sliver_bangumi_list.dart @@ -11,14 +11,15 @@ 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'; typedef HandleSubscribe = void Function(Bangumi bangumi, String flag); @immutable -class BangumiSliverGridFragment extends StatelessWidget { - const BangumiSliverGridFragment({ +class SliverBangumiList extends StatelessWidget { + const SliverBangumiList({ super.key, this.flag, required this.bangumis, @@ -94,51 +95,54 @@ class BangumiSliverGridFragment extends StatelessWidget { Bangumi bangumi, ) { final currFlag = '$flag:bangumi:${bangumi.id}:${bangumi.cover}'; - final cover = _buildBangumiItemCover(imageWidth, currFlag, bangumi); - return 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) - 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, + 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) + 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, + ), ), ), ), + PositionedDirectional( + child: _buildSubscribeButton(theme, bangumi, currFlag), ), - PositionedDirectional( - child: _buildSubscribeButton(theme, bangumi, currFlag), - ), - ], + ], + ), ), ); } @@ -162,76 +166,79 @@ class BangumiSliverGridFragment extends StatelessWidget { stops: [0.68, 1.0], ), ), - child: _buildBangumiItemCover(imageWidth, currFlag, bangumi), + child: _buildBangumiItemCover(imageWidth, bangumi), ); - return 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) - 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, + 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) + 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, + ), ), ), ), + PositionedDirectional( + child: _buildSubscribeButton(theme, bangumi, currFlag), ), - 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( + children: [ Text( - bangumi.updateAt, + bangumi.name, maxLines: 1, overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall! - .copyWith(color: Colors.white70), + style: theme.textTheme.titleSmall! + .copyWith(color: Colors.white), ), - ], + if (bangumi.updateAt.isNotBlank) + Text( + bangumi.updateAt, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall! + .copyWith(color: Colors.white70), + ), + ], + ), ), - ), - ], + ], + ), ), ); } @@ -243,60 +250,64 @@ class BangumiSliverGridFragment extends StatelessWidget { Bangumi bangumi, ) { final currFlag = '$flag:bangumi:${bangumi.id}:${bangumi.cover}'; - final cover = _buildBangumiItemCover(imageWidth, currFlag, bangumi); + final cover = _buildBangumiItemCover(imageWidth, bangumi); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( - 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) - 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: 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) + 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, + ), ), ), ), + PositionedDirectional( + child: + _buildSubscribeButton(theme, bangumi, currFlag), ), - PositionedDirectional( - child: _buildSubscribeButton(theme, bangumi, currFlag), - ), - ], - ), + ], + ), + ), ), ), sizedBoxH8, @@ -325,56 +336,59 @@ class BangumiSliverGridFragment extends StatelessWidget { Bangumi bangumi, ) { final currFlag = '$flag:bangumi:${bangumi.id}:${bangumi.cover}'; - final cover = _buildBangumiItemCover(imageWidth, currFlag, bangumi); + final cover = _buildBangumiItemCover(imageWidth, bangumi); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( - 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: 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, + ), ), ), ), - ), - ], - ), + ], + ), + ), ), ), sizedBoxH8, @@ -456,33 +470,38 @@ class BangumiSliverGridFragment extends StatelessWidget { Widget _buildBangumiItemCover( int cacheWidth, - String currFlag, Bangumi bangumi, ) { - return Hero( - tag: currFlag, - child: FadeInImage( - placeholder: const AssetImage('assets/mikan.png'), - image: ResizeImage.resizeIfNeeded( - cacheWidth, - null, - CacheImage(bangumi.cover), - ), - fit: BoxFit.cover, - imageErrorBuilder: (_, __, ___) { - return _buildBangumiItemError(); - }, + final image = FadeInImage( + placeholder: Assets.mikan.provider(), + image: ResizeImage.resizeIfNeeded( + cacheWidth, + null, + CacheImage(bangumi.cover), ), + fit: BoxFit.cover, + imageErrorBuilder: (_, __, ___) { + return _buildBangumiItemError(); + }, ); + return bangumi.grey + ? ColorFiltered( + colorFilter: const ColorFilter.mode( + Colors.grey, + BlendMode.saturation, + ), + child: image, + ) + : image; } Widget _buildBangumiItemError() { return Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( image: DecorationImage( - image: AssetImage('assets/mikan.png'), + image: Assets.mikan.provider(), fit: BoxFit.cover, - colorFilter: ColorFilter.mode(Colors.grey, BlendMode.color), + colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.color), ), ), ); diff --git a/lib/ui/fragments/subgroup_bangumis.dart b/lib/ui/fragments/subgroup_bangumis.dart index 342dda8..b5d733d 100644 --- a/lib/ui/fragments/subgroup_bangumis.dart +++ b/lib/ui/fragments/subgroup_bangumis.dart @@ -46,14 +46,21 @@ class SubgroupBangumis extends StatelessWidget { SliverPinnedAppBar( title: subgroupBangumi.name, actions: [ + if (!subgroupBangumi.rss.isNullOrBlank) + IconButton( + onPressed: () { + subgroupBangumi.rss.copy(); + }, + icon: const Icon(Icons.rss_feed_rounded), + ), IconButton( tooltip: '查看字幕组', onPressed: () { final subgroups = subgroupBangumi.subgroups; showSelectSubgroupPanel(context, subgroups); }, - icon: const Icon(Icons.person_outline_rounded), - ) + icon: const Icon(Icons.group_rounded), + ), ], ), _buildList( diff --git a/lib/ui/fragments/subgroup_subscribe.dart b/lib/ui/fragments/subgroup_subscribe.dart new file mode 100644 index 0000000..2d8012d --- /dev/null +++ b/lib/ui/fragments/subgroup_subscribe.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../internal/extension.dart'; +import '../../internal/method.dart'; +import '../../internal/repo.dart'; +import '../../providers/bangumi_model.dart'; +import '../../topvars.dart'; +import '../../widget/sliver_pinned_header.dart'; + +class SubgroupSubscribe extends StatelessWidget { + const SubgroupSubscribe(this.model, {super.key}); + + final BangumiModel model; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ChangeNotifierProvider.value( + value: model, + child: Scaffold( + body: CustomScrollView( + slivers: [ + SliverPinnedAppBar( + title: '字幕组订阅', + actions: [ + IconButton( + onPressed: () => wrapLoading(model.changeSubscribe), + icon: Selector( + selector: (_, model) => + model.bangumiDetail?.subscribed ?? false, + shouldRebuild: (pre, next) => pre != next, + builder: (_, subscribed, __) { + return Icon( + subscribed + ? Icons.favorite_rounded + : Icons.favorite_border_rounded, + color: subscribed ? theme.colorScheme.secondary : null, + ); + }, + ), + ), + ], + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Text( + '注:\n仅会显示订阅/RSS时适用,番组详情列表仍为全部条目。\n选择语言时未选中对应的值,说明当前字幕组不支持订阅选择的语言', + style: theme.textTheme.bodyMedium, + ), + ), + ), + Consumer( + builder: (context, model, child) { + final subgroups = model.bangumiDetail!.subgroupBangumis.values + .toList(growable: false); + return SliverList.builder( + itemBuilder: (context, index) { + final sub = subgroups[index]; + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 10.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Text( + sub.name, + style: theme.textTheme.titleMedium, + ), + ), + if (!sub.rss.isNullOrBlank) + ElevatedButton( + onPressed: () { + sub.rss.copy(); + }, + style: ElevatedButton.styleFrom( + minimumSize: const Size(32.0, 32.0), + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + ), + shape: const RoundedRectangleBorder( + borderRadius: borderRadius8, + ), + ), + child: sub.subscribed + ? Row( + children: [ + const Icon(Icons.rss_feed_rounded), + sizedBoxW4, + Text(sub.sublang!), + ], + ) + : const Icon(Icons.rss_feed_rounded), + ), + ], + ), + sizedBoxH4, + SegmentedButton( + showSelectedIcon: false, + segments: const [ + ButtonSegment(value: 0, label: Text('全部')), + ButtonSegment(value: 1, label: Text('简中')), + ButtonSegment(value: 2, label: Text('繁中')), + ButtonSegment(value: -1, label: Text('退订')), + ], + selected: {sub.state}, + style: ButtonStyle( + shape: + MaterialStateProperty.resolveWith((states) { + return const RoundedRectangleBorder( + borderRadius: borderRadius12, + ); + }), + ), + onSelectionChanged: (v) { + wrapLoading(() async { + final x = v.first; + if (x == -1) { + await Repo.subscribeBangumi( + int.parse(model.id), + true, + subgroupId: int.tryParse(sub.dataId), + ); + } else { + await Repo.subscribeBangumi( + int.parse(model.id), + false, + subgroupId: int.tryParse(sub.dataId), + language: x == 0 ? null : x, + ); + } + await model.load(); + }); + }, + ), + ], + ), + ); + }, + itemCount: subgroups.length, + ); + }, + ), + sliverSizedBoxH24WithNavBarHeight(context), + ], + ), + ), + ); + } +} diff --git a/lib/ui/fragments/subscribed.dart b/lib/ui/fragments/subscribed.dart index e4dcd54..ac5cd5c 100644 --- a/lib/ui/fragments/subscribed.dart +++ b/lib/ui/fragments/subscribed.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; +import 'package:infinite_carousel/infinite_carousel.dart'; import 'package:provider/provider.dart'; import 'package:sliver_tools/sliver_tools.dart'; @@ -13,16 +14,17 @@ import '../../mikan_routes.dart'; import '../../model/bangumi.dart'; import '../../model/record_item.dart'; import '../../model/season_gallery.dart'; +import '../../providers/index_model.dart'; import '../../providers/op_model.dart'; import '../../providers/subscribed_model.dart'; +import '../../res/assets.gen.dart'; import '../../topvars.dart'; -import '../../widget/infinite_carousel.dart'; import '../../widget/scalable_tap.dart'; import '../../widget/sliver_pinned_header.dart'; import '../components/rss_record_item.dart'; -import 'bangumi_sliver_grid.dart'; import 'index.dart'; import 'select_tablet_mode.dart'; +import 'sliver_bangumi_list.dart'; @immutable class SubscribedFragment extends StatefulWidget { @@ -43,7 +45,7 @@ class _SubscribedFragmentState extends LifecycleAppState { _timer = Timer.periodic(const Duration(milliseconds: 3600), (timer) { if (_infiniteScrollController.hasClients) { _infiniteScrollController.animateToItem( - (_infiniteScrollController.offset / 300.0).round() + 1, + (_infiniteScrollController.offset / 280.0).round() + 1, duration: const Duration(milliseconds: 800), curve: Curves.easeInOut, ); @@ -127,18 +129,18 @@ class _SubscribedFragmentState extends LifecycleAppState { return Selector?>( selector: (_, model) => model.bangumis, builder: (context, bangumis, __) { - if (subscribedModel.seasonLoading) { + if (bangumis == null) { return _buildLoading(); } - if (bangumis.isNullOrEmpty) { + if (bangumis.isEmpty) { return _buildEmpty( theme, '本季度您还没有订阅任何番组哦\n快去添加订阅吧', ); } - return BangumiSliverGridFragment( + return SliverBangumiList( flag: 'subscribed', - bangumis: bangumis!, + bangumis: bangumis, handleSubscribe: (bangumi, flag) { context.read().subscribeBangumi( bangumi.id, @@ -183,12 +185,12 @@ class _SubscribedFragmentState extends LifecycleAppState { Tooltip( message: [ if (updateNum! > 0) '最近有更新 $updateNum部', - '本季度共订阅 ${bangumis!.length}部' + '本季度共订阅 ${bangumis!.length}部', ].join(','), child: Text( [ if (updateNum > 0) '🚀 $updateNum部', - '🎬 ${bangumis.length}部' + '🎬 ${bangumis.length}部', ].join(','), style: theme.textTheme.bodySmall, ), @@ -209,7 +211,7 @@ class _SubscribedFragmentState extends LifecycleAppState { season: subscribedModel.season!.season, title: subscribedModel.season!.title, bangumis: subscribedModel.bangumis ?? [], - ) + ), ], ), ); @@ -276,21 +278,20 @@ class _SubscribedFragmentState extends LifecycleAppState { ) { return Selector>?>( selector: (_, model) => model.rss, - shouldRebuild: (pre, next) => pre.ne(next), builder: (_, rss, __) { - if (subscribedModel.recordsLoading) { + if (rss == null) { return _buildLoading(); } - if (rss.isNullOrEmpty) { + if (rss.isEmpty) { return _buildEmpty( theme, '您的订阅中最近三天还没有更新内容哦\n快去添加订阅吧', ); } - final entries = rss!.entries.toList(growable: false); + final entries = rss.entries.toList(growable: false); return SliverToBoxAdapter( child: SizedBox( - height: 136.0, + height: 200.0, child: InfiniteCarousel.builder( itemBuilder: (context, index, realIndex) { final entry = entries[index]; @@ -300,7 +301,7 @@ class _SubscribedFragmentState extends LifecycleAppState { itemExtent: 280.0, itemCount: entries.length, center: false, - velocityFactor: 0.8, + velocityFactor: 1.0, ), ), ); @@ -319,10 +320,7 @@ class _SubscribedFragmentState extends LifecycleAppState { child: Center( child: Column( children: [ - Image.asset( - 'assets/mikan.png', - width: 64.0, - ), + Assets.mikan.image(width: 64.0), sizedBoxH12, Text( text, @@ -378,100 +376,96 @@ class _SubscribedFragmentState extends LifecycleAppState { top: 8.0, bottom: 8.0, ), - child: Stack( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Positioned.fill( - child: ScalableCard( - onTap: () { - Navigator.pushNamed( - context, - Routes.bangumi.name, - arguments: Routes.bangumi.d( - heroTag: currFlag, - bangumiId: bangumiId, - cover: bangumiCover, - title: record.name, + 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.resizeIfNeeded( + (280.0 * context.devicePixelRatio).ceil(), + null, + CacheImage(bangumiCover), + ), + fit: BoxFit.cover, + ), + ), + ), ), - ); - }, - child: Hero( - tag: currFlag, - child: Tooltip( - message: records.first.name, - child: FadeInImage( - placeholder: const AssetImage( - 'assets/mikan.png', + ), + PositionedDirectional( + end: 12.0, + top: 12.0, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: theme.colorScheme.error, + shape: const StadiumBorder(), ), - image: ResizeImage.resizeIfNeeded( - (280.0 * context.devicePixelRatio).ceil(), - null, - CacheImage(bangumiCover), + padding: edgeH6V2, + child: Text( + badge, + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onError, + ), ), - fit: BoxFit.cover, ), ), - ), + ], ), ), - PositionedDirectional( - top: 12.0, - end: 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, + Text( + record.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleSmall!.copyWith( + color: Colors.white, + shadows: [ + const BoxShadow( + color: Colors.black38, + blurRadius: 2.0, + spreadRadius: 2.0, ), - ), + ], ), ), - PositionedDirectional( - bottom: 12.0, - start: 12.0, - end: 12.0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - record.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleSmall!.copyWith( - color: Colors.white, - shadows: [ - const BoxShadow( - color: Colors.black38, - blurRadius: 2.0, - spreadRadius: 2.0, - ), - ], - ), - ), - if (record.publishAt.isNotBlank) - Text( - record.publishAt, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall!.copyWith( - color: Colors.white.withOpacity(0.87), - shadows: [ - const BoxShadow( - color: Colors.black38, - blurRadius: 2.0, - spreadRadius: 2.0, - ), - ], - ), + if (record.publishAt.isNotBlank) + Text( + record.publishAt, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall!.copyWith( + color: Colors.white.withOpacity(0.87), + shadows: [ + const BoxShadow( + color: Colors.black38, + blurRadius: 2.0, + spreadRadius: 2.0, ), - ], + ], + ), ), - ), ], ), ); @@ -592,22 +586,34 @@ class _PinedHeader extends StatelessWidget { return SliverPinnedAppBar( title: '我的订阅', autoImplLeading: false, - actions: isTablet - ? null - : [ - IconButton( - onPressed: () { - Navigator.pushNamed(context, Routes.announcements.name); - }, - icon: const Icon(Icons.notifications_none_rounded), - ), - IconButton( - onPressed: () { - showSettingsPanel(context); - }, - icon: const Icon(Icons.tune_rounded), - ), - ], + actions: [ + Selector( + selector: (_, model) => model.user?.rss, + builder: (context, rss, child) { + if (rss.isNullOrBlank) { + return const SizedBox.shrink(); + } + return IconButton( + onPressed: () { + rss.copy(); + }, + icon: const Icon(Icons.rss_feed_rounded), + ); + }, + ), + IconButton( + onPressed: () { + Navigator.pushNamed(context, Routes.announcements.name); + }, + icon: const Icon(Icons.notifications_none_rounded), + ), + IconButton( + onPressed: () { + showSettingsPanel(context); + }, + icon: const Icon(Icons.tune_rounded), + ), + ], ); }, ); diff --git a/lib/ui/fragments/theme_color.dart b/lib/ui/fragments/theme_color.dart index 2f6ca04..5f89221 100644 --- a/lib/ui/fragments/theme_color.dart +++ b/lib/ui/fragments/theme_color.dart @@ -47,10 +47,7 @@ class _ThemeColorPanelState extends LifecycleAppState { return Scaffold( body: CustomScrollView( slivers: [ - const SliverPinnedAppBar( - title: '选择主题色', - borderRadius: borderRadiusT28, - ), + const SliverPinnedAppBar(title: '选择主题色'), if (_colorSchemePair != null) SliverToBoxAdapter( child: Padding( @@ -80,7 +77,7 @@ class _ThemeColorPanelState extends LifecycleAppState { value: v, ); }, - ) + ), ], ), ), diff --git a/lib/ui/pages/announcement.dart b/lib/ui/pages/announcement.dart index 79de422..235e986 100644 --- a/lib/ui/pages/announcement.dart +++ b/lib/ui/pages/announcement.dart @@ -8,6 +8,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import '../../internal/extension.dart'; import '../../model/announcement.dart'; import '../../providers/index_model.dart'; +import '../../res/assets.gen.dart'; import '../../topvars.dart'; import '../../widget/placeholder_text.dart'; import '../../widget/sliver_pinned_header.dart'; @@ -36,10 +37,7 @@ class Announcements extends StatelessWidget { child: Center( child: Column( children: [ - Image.asset( - 'assets/mikan.png', - width: 64.0, - ), + Assets.mikan.image(width: 64.0), sizedBoxH12, Text( '暂无数据', diff --git a/lib/ui/pages/bangumi.dart b/lib/ui/pages/bangumi.dart index cf81a92..c7985b2 100644 --- a/lib/ui/pages/bangumi.dart +++ b/lib/ui/pages/bangumi.dart @@ -12,12 +12,14 @@ import '../../internal/image_provider.dart'; import '../../internal/kit.dart'; import '../../mikan_routes.dart'; import '../../providers/bangumi_model.dart'; +import '../../res/assets.gen.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 '../fragments/subgroup_subscribe.dart'; @FFRoute(name: '/bangumi') @immutable @@ -179,118 +181,144 @@ class BangumiPage extends StatelessWidget { for (final e in subgroups) { final length = e.value.records.length; final maxItemLen = length > 4 ? 4 : length; - subList.addAll([ - sizedBoxH24, - Row( - children: [ - Expanded( - child: Text( - e.value.name, - style: theme.textTheme.titleLarge, + subList.addAll( + [ + sizedBoxH24, + Row( + children: [ + Expanded( + child: Text( + e.value.name, + style: theme.textTheme.titleLarge, + ), ), - ), - Transform.translate( - offset: const Offset(12.0, 0.0), - child: IconButton( - onPressed: () { - _showSubgroupPanel(context, model, e.value.dataId); - }, - icon: const Icon(Icons.east_rounded), + sizedBoxW8, + if (!e.value.rss.isNullOrBlank) + ElevatedButton( + onPressed: () { + e.value.rss.copy(); + }, + style: ElevatedButton.styleFrom( + minimumSize: const Size(32.0, 32.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0), + shape: const RoundedRectangleBorder( + borderRadius: borderRadius8, + ), + ), + child: e.value.subscribed + ? Row( + children: [ + const Icon(Icons.rss_feed_rounded), + sizedBoxW4, + Text(e.value.sublang!), + ], + ) + : const Icon(Icons.rss_feed_rounded), + ), + Transform.translate( + offset: const Offset(12.0, 0.0), + child: IconButton( + onPressed: () { + _showSubgroupPanel(context, model, e.value.dataId); + }, + icon: const Icon(Icons.east_rounded), + ), ), - ), - ], - ), - sizedBoxH12, - for (int index = 0; index < maxItemLen; index++) - () { - final record = e.value.records[index]; - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: ScalableCard( - onTap: () { - Navigator.pushNamed( - context, - Routes.record.name, - arguments: Routes.record.d(url: record.url), - ); - }, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - record.title, - style: theme.textTheme.bodyMedium, - ), - sizedBoxH12, - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Wrap( - runSpacing: 4.0, - spacing: 4.0, - children: [ - if (record.size.isNotBlank) - Container( - padding: edgeH4V2, - decoration: BoxDecoration( - color: theme.secondary, - borderRadius: borderRadius4, + ], + ), + sizedBoxH12, + for (int index = 0; index < maxItemLen; index++) + () { + final record = e.value.records[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: ScalableCard( + onTap: () { + Navigator.pushNamed( + context, + Routes.record.name, + arguments: Routes.record.d(url: record.url), + ); + }, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + record.title, + style: theme.textTheme.bodyMedium, + ), + sizedBoxH12, + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Wrap( + runSpacing: 4.0, + spacing: 4.0, + children: [ + if (record.size.isNotBlank) + Container( + padding: edgeH4V2, + decoration: BoxDecoration( + color: theme.secondary, + borderRadius: borderRadius4, + ), + child: Text( + record.size, + style: accentTagStyle, + ), ), - child: Text( - record.size, - style: accentTagStyle, + if (!record.tags.isNullOrEmpty) + ...List.generate( + record.tags.length, + (index) { + return Container( + padding: edgeH4V2, + decoration: BoxDecoration( + color: theme.primary, + borderRadius: + borderRadius4, + ), + child: Text( + record.tags[index], + style: primaryTagStyle, + ), + ); + }, ), - ), - if (!record.tags.isNullOrEmpty) - ...List.generate( - record.tags.length, - (index) { - return Container( - padding: edgeH4V2, - decoration: BoxDecoration( - color: theme.primary, - borderRadius: borderRadius4, - ), - child: Text( - record.tags[index], - style: primaryTagStyle, - ), - ); - }, - ), - ], - ), - sizedBoxH4, - Text( - record.publishAt, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall, - ), - ], + ], + ), + sizedBoxH4, + Text( + record.publishAt, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall, + ), + ], + ), ), - ), - sizedBoxW8, - TMSMenuButton( - torrent: record.torrent, - magnet: record.magnet, - share: record.share, - ), - ], - ), - ], + sizedBoxW8, + TMSMenuButton( + torrent: record.torrent, + magnet: record.magnet, + share: record.share, + ), + ], + ), + ], + ), ), ), - ), - ); - }() - ]); + ); + }(), + ], + ); subTags.add( Tooltip( message: e.value.name, @@ -318,7 +346,7 @@ class BangumiPage extends StatelessWidget { } } - final scale = (50.0 + context.screenWidth) / context.screenWidth; + final scale = (64.0 + context.screenWidth) / context.screenWidth; final items = [ Stack( children: [ @@ -399,7 +427,7 @@ class BangumiPage extends StatelessWidget { e.value, softWrap: true, style: theme.textTheme.labelLarge, - ) + ), ], ); return index == detail.more.length - 1 @@ -421,7 +449,7 @@ class BangumiPage extends StatelessWidget { '$title\n', style: theme.textTheme.titleLarge ?.copyWith(color: theme.secondary), - maxLines: 2, + maxLines: 3, overflow: TextOverflow.ellipsis, ), ), @@ -439,11 +467,36 @@ class BangumiPage extends StatelessWidget { ), ), if (subTags.isNotEmpty) ...[ - Text( - '字幕组', - style: theme.textTheme.titleLarge, + Row( + children: [ + Expanded( + child: Text( + '字幕组', + style: theme.textTheme.titleLarge, + ), + ), + ElevatedButton( + onPressed: () { + MBottomSheet.show( + context, + (context) => MBottomSheet( + heightFactor: 0.78, + child: SubgroupSubscribe(model), + ), + ); + }, + style: ElevatedButton.styleFrom( + minimumSize: const Size(32.0, 32.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0), + shape: const RoundedRectangleBorder( + borderRadius: borderRadius8, + ), + ), + child: const Icon(Icons.edit_note_rounded), + ), + ], ), - sizedBoxH12, + sizedBoxH4, Wrap( spacing: 8.0, runSpacing: 8.0, @@ -490,52 +543,42 @@ class BangumiPage extends StatelessWidget { } Widget _buildCover(String cover) { - return ScalableCard( - onTap: () {}, - child: Image( - image: CacheImage(cover), - width: 148.0, - loadingBuilder: (_, child, event) { - return event == null - ? child - : AspectRatio( - aspectRatio: 3 / 4, - child: Hero( - tag: heroTag, + 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: Image.asset( - 'assets/mikan.png', - ), + child: Assets.mikan.image(), ), ), - ), - ); - }, - errorBuilder: (_, __, ___) { - return AspectRatio( - aspectRatio: 3 / 4, - child: Hero( - tag: heroTag, + ); + }, + errorBuilder: (_, __, ___) { + return AspectRatio( + aspectRatio: 3 / 4, child: Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( image: DecorationImage( - image: AssetImage('assets/mikan.png'), + image: Assets.mikan.provider(), fit: BoxFit.cover, - colorFilter: ColorFilter.mode(Colors.grey, BlendMode.color), + colorFilter: + const ColorFilter.mode(Colors.grey, BlendMode.color), ), ), ), - ), - ); - }, - frameBuilder: (_, child, ___, ____) { - return Hero( - tag: heroTag, - child: child, - ); - }, + ); + }, + ), ), ); } diff --git a/lib/ui/pages/fonts.dart b/lib/ui/pages/fonts.dart index d1e6855..356bbc9 100644 --- a/lib/ui/pages/fonts.dart +++ b/lib/ui/pages/fonts.dart @@ -45,7 +45,7 @@ class Fonts extends StatelessWidget { icon: const Icon(Icons.restart_alt_rounded), onPressed: fontsModel.resetDefaultFont, ), - ) + ), ], ), _buildList( @@ -127,7 +127,7 @@ class Fonts extends StatelessWidget { ), ), sizedBoxW4, - _buildLoadingOrChecked(theme, model, font) + _buildLoadingOrChecked(theme, model, font), ], ), sizedBoxH4, diff --git a/lib/ui/pages/forgot_password.dart b/lib/ui/pages/forgot_password.dart index 25bbc3e..dd7a6fe 100644 --- a/lib/ui/pages/forgot_password.dart +++ b/lib/ui/pages/forgot_password.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import '../../internal/extension.dart'; import '../../providers/forgot_password_model.dart'; +import '../../res/assets.gen.dart'; import '../../topvars.dart'; import '../../widget/bottom_sheet.dart'; import '../fragments/forgot_password_confirm.dart'; @@ -43,7 +44,7 @@ class _ForgotPasswordPageState extends State { child: Column( children: [ Image.asset( - 'assets/mikan.png', + Assets.mikan.path, width: 64.0, ), sizedBoxH8, diff --git a/lib/ui/pages/license.dart b/lib/ui/pages/license.dart index 5158061..6e8cb8c 100644 --- a/lib/ui/pages/license.dart +++ b/lib/ui/pages/license.dart @@ -7,6 +7,7 @@ import '../../internal/delegate.dart'; import '../../internal/extension.dart'; import '../../internal/kit.dart'; import '../../mikan_routes.dart'; +import '../../res/assets.gen.dart'; import '../../topvars.dart'; import '../../widget/scalable_tap.dart'; import '../../widget/sliver_pinned_header.dart'; @@ -41,12 +42,10 @@ class LicenseList extends StatelessWidget { child: Container( height: 120.0, width: 120.0, - decoration: const BoxDecoration( + decoration: BoxDecoration( image: DecorationImage( - image: AssetImage( - 'assets/mikan.png', - ), - colorFilter: ColorFilter.mode( + image: Assets.mikan.provider(), + colorFilter: const ColorFilter.mode( Colors.grey, BlendMode.color, ), @@ -146,7 +145,7 @@ class LicenseList extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Image.asset( - 'assets/mikan.png', + Assets.mikan.path, width: 36.0, ), sizedBoxW12, diff --git a/lib/ui/pages/license_detail.dart b/lib/ui/pages/license_detail.dart index eda9506..2138bb3 100644 --- a/lib/ui/pages/license_detail.dart +++ b/lib/ui/pages/license_detail.dart @@ -6,6 +6,7 @@ import 'package:flutter/scheduler.dart'; import '../../internal/extension.dart'; import '../../internal/kit.dart'; +import '../../res/assets.gen.dart'; import '../../widget/sliver_pinned_header.dart'; @FFRoute(name: '/license/detail') @@ -52,12 +53,10 @@ class _LicenseDetailState extends State { child: Container( height: 120.0, width: 120.0, - decoration: const BoxDecoration( + decoration: BoxDecoration( image: DecorationImage( - image: AssetImage( - 'assets/mikan.png', - ), - colorFilter: ColorFilter.mode( + image: Assets.mikan.provider(), + colorFilter: const ColorFilter.mode( Colors.grey, BlendMode.color, ), diff --git a/lib/ui/pages/login.dart b/lib/ui/pages/login.dart index 1ffcd07..0f60e0b 100644 --- a/lib/ui/pages/login.dart +++ b/lib/ui/pages/login.dart @@ -7,6 +7,7 @@ import '../../mikan_routes.dart'; import '../../providers/index_model.dart'; import '../../providers/login_model.dart'; import '../../providers/subscribed_model.dart'; +import '../../res/assets.gen.dart'; import '../../topvars.dart'; @FFRoute(name: '/login') @@ -42,10 +43,7 @@ class _LoginPageState extends State { key: _formKey, child: Column( children: [ - Image.asset( - 'assets/mikan.png', - width: 64.0, - ), + Assets.mikan.image(width: 64.0), sizedBoxH8, Text( 'Mikan Project', @@ -151,7 +149,7 @@ class _LoginPageState extends State { Navigator.of(context).pushNamed(Routes.forgetPassword.name); }, child: const Text('忘记密码'), - ) + ), ], ); } diff --git a/lib/ui/pages/record.dart b/lib/ui/pages/record.dart index e38fedd..e051958 100644 --- a/lib/ui/pages/record.dart +++ b/lib/ui/pages/record.dart @@ -14,6 +14,7 @@ import '../../internal/kit.dart'; import '../../model/record_details.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'; @@ -313,9 +314,7 @@ class Record extends StatelessWidget { child: Container( padding: edge28, child: Center( - child: Image.asset( - 'assets/mikan.png', - ), + child: Assets.mikan.image(), ), ), ); @@ -324,11 +323,14 @@ class Record extends StatelessWidget { return AspectRatio( aspectRatio: 3 / 4, child: Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( image: DecorationImage( - image: AssetImage('assets/mikan.png'), + image: Assets.mikan.provider(), fit: BoxFit.cover, - colorFilter: ColorFilter.mode(Colors.grey, BlendMode.color), + colorFilter: const ColorFilter.mode( + Colors.grey, + BlendMode.color, + ), ), ), ), @@ -404,7 +406,7 @@ class Record extends StatelessWidget { color: Colors.grey.withOpacity(0.24), child: Center( child: Image.asset( - 'assets/mikan.png', + Assets.mikan.path, width: 56.0, ), ), diff --git a/lib/ui/pages/register.dart b/lib/ui/pages/register.dart index 1c1b65c..32f7a3e 100644 --- a/lib/ui/pages/register.dart +++ b/lib/ui/pages/register.dart @@ -7,6 +7,7 @@ import '../../mikan_routes.dart'; import '../../providers/index_model.dart'; import '../../providers/register_model.dart'; import '../../providers/subscribed_model.dart'; +import '../../res/assets.gen.dart'; import '../../topvars.dart'; @FFRoute(name: '/register') @@ -43,10 +44,7 @@ class _RegisterPageState extends State { key: _formKey, child: Column( children: [ - Image.asset( - 'assets/mikan.png', - width: 64.0, - ), + Assets.mikan.image(width: 64.0), sizedBoxH8, Text( 'Mikan Project', @@ -76,7 +74,7 @@ class _RegisterPageState extends State { child: const Icon(Icons.west_rounded), ), sizedBoxW16, - Expanded(child: _buildRegisterButton(theme)) + Expanded(child: _buildRegisterButton(theme)), ], ), sizedBoxH56, diff --git a/lib/ui/pages/search.dart b/lib/ui/pages/search.dart index f8f427d..0465c91 100644 --- a/lib/ui/pages/search.dart +++ b/lib/ui/pages/search.dart @@ -1,20 +1,22 @@ import 'package:ff_annotation_route_core/ff_annotation_route_core.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:provider/provider.dart'; import 'package:waterfall_flow/waterfall_flow.dart'; +import '../../internal/consts.dart'; import '../../internal/delegate.dart'; import '../../internal/extension.dart'; 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'; import '../../providers/search_model.dart'; +import '../../res/assets.gen.dart'; import '../../topvars.dart'; import '../../widget/ripple_tap.dart'; import '../../widget/scalable_tap.dart'; @@ -22,8 +24,8 @@ import '../../widget/sliver_pinned_header.dart'; import '../components/simple_record_item.dart'; @FFRoute(name: '/search') -class SearchFragment extends StatelessWidget { - const SearchFragment({super.key}); +class Search extends StatelessWidget { + const Search({super.key}); @override Widget build(BuildContext context) { @@ -39,7 +41,28 @@ class SearchFragment extends StatelessWidget { return Scaffold( body: CustomScrollView( slivers: [ - const SliverPinnedAppBar(title: '搜索'), + SliverPinnedAppBar( + title: '搜索', + actions: [ + Selector( + selector: (_, model) => model.loading, + shouldRebuild: (pre, next) => pre != next, + builder: (_, loading, __) { + if (loading) { + return IconButton( + icon: const SizedBox( + width: 24.0, + height: 24.0, + child: CircularProgressIndicator(), + ), + onPressed: () {}, + ); + } + return sizedBox; + }, + ), + ], + ), _buildHeaderSearchField(theme, searchModel), _buildSearchHistory(theme, searchModel), _buildSubgroupSection(theme), @@ -242,7 +265,9 @@ class SearchFragment extends StatelessWidget { builder: (_, subgroupId, __) { final selected = subgroup.id == subgroupId; return RippleTap( - color: selected ? theme.primary : theme.secondary, + color: selected + ? theme.primary.withOpacity(0.38) + : theme.secondary.withOpacity(0.1), borderRadius: borderRadius8, onTap: () { searchModel.subgroupId = subgroup.id; @@ -250,17 +275,13 @@ class SearchFragment extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric( horizontal: 12.0, - vertical: 6.0, + vertical: 8.0, ), child: Text( subgroup.name, maxLines: 1, overflow: TextOverflow.ellipsis, - style: theme.textTheme.labelLarge!.copyWith( - color: selected - ? theme.colorScheme.onPrimary - : theme.colorScheme.onSecondary, - ), + style: theme.textTheme.labelLarge, ), ), ); @@ -298,32 +319,57 @@ class SearchFragment extends StatelessWidget { child: TextField( decoration: InputDecoration( labelText: '请输入关键字', - prefixIcon: const Icon(Icons.search_rounded), + prefixIcon: const Icon( + Icons.search_rounded, + size: 24.0, + ), isDense: true, border: const OutlineInputBorder(), - suffixIcon: Selector( - selector: (_, model) => model.loading, - shouldRebuild: (pre, next) => pre != next, - builder: (_, loading, __) { - if (loading) { - return const CupertinoActivityIndicator(); - } - return ValueListenableBuilder( - valueListenable: searchModel.keywordsController, - builder: (context, v, child) { - if (v.text.isNotEmpty) { - return IconButton( - onPressed: () { - searchModel.keywordsController.clear(); - }, - icon: const Icon(Icons.clear), - ); - } - return sizedBox; - }, - ); - }, + suffixIcon: Padding( + padding: const EdgeInsetsDirectional.only(end: 8.0), + child: ValueListenableBuilder( + valueListenable: searchModel.keywordsController, + builder: (context, v, child) { + if (v.text.isNotEmpty) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () { + searchModel.keywordsController.clear(); + }, + icon: const Icon( + Icons.clear_rounded, + size: 24.0, + ), + ), + IconButton( + icon: const Icon( + Icons.rss_feed_rounded, + size: 24.0, + ), + onPressed: () { + '${MikanUrls.baseUrl}/RSS/Search?searcher=${Uri.encodeComponent(searchModel.keywordsController.text)}' + .copy(); + }, + ), + ], + ); + } + return IconButton( + icon: const Icon( + Icons.rss_feed_rounded, + size: 24.0, + ), + onPressed: () { + '${MikanUrls.baseUrl}/RSS/Search?searcher=${Uri.encodeComponent(searchModel.keywordsController.text)}' + .copy(); + }, + ); + }, + ), ), + suffixIconConstraints: const BoxConstraints(), ), autofocus: true, textInputAction: TextInputAction.search, @@ -376,9 +422,7 @@ class SearchFragment extends StatelessWidget { child: Container( padding: edge28, child: Center( - child: Image.asset( - 'assets/mikan.png', - ), + child: Assets.mikan.image(), ), ), ); @@ -387,11 +431,11 @@ class SearchFragment extends StatelessWidget { return Hero( tag: currFlag, child: Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( image: DecorationImage( - image: AssetImage('assets/mikan.png'), + image: Assets.mikan.provider(), fit: BoxFit.cover, - colorFilter: ColorFilter.mode( + colorFilter: const ColorFilter.mode( Colors.grey, BlendMode.color, ), @@ -451,6 +495,7 @@ class SearchFragment extends StatelessWidget { color: theme.primary.withOpacity(0.1), borderRadius: borderRadius8, onTap: () { + hideKeyboard(); searchModel.search(it); }, child: Padding( diff --git a/lib/ui/pages/season_bangumi.dart b/lib/ui/pages/season_bangumi.dart index f26fdaa..1195b5c 100644 --- a/lib/ui/pages/season_bangumi.dart +++ b/lib/ui/pages/season_bangumi.dart @@ -14,7 +14,7 @@ import '../../providers/op_model.dart'; import '../../providers/season_list_model.dart'; import '../../topvars.dart'; import '../../widget/sliver_pinned_header.dart'; -import '../fragments/bangumi_sliver_grid.dart'; +import '../fragments/sliver_bangumi_list.dart'; @FFRoute(name: '/bangumi/season') @immutable @@ -67,7 +67,7 @@ class SeasonBangumi extends StatelessWidget { theme, bangumiRow, ), - BangumiSliverGridFragment( + SliverBangumiList( flag: seasonTitle, bangumis: bangumiRow.bangumis, handleSubscribe: (bangumi, flag) { @@ -133,14 +133,14 @@ class SeasonBangumi extends StatelessWidget { if (bangumiRow.subscribedUpdatedNum > 0) '💖 ${bangumiRow.subscribedUpdatedNum}部', if (bangumiRow.subscribedNum > 0) '❤ ${bangumiRow.subscribedNum}部', - '🎬 ${bangumiRow.num}部' + '🎬 ${bangumiRow.num}部', ].join(','); final full = [ if (bangumiRow.updatedNum > 0) '更新${bangumiRow.updatedNum}部', if (bangumiRow.subscribedUpdatedNum > 0) '订阅更新${bangumiRow.subscribedUpdatedNum}部', if (bangumiRow.subscribedNum > 0) '订阅${bangumiRow.subscribedNum}部', - '共${bangumiRow.num}部' + '共${bangumiRow.num}部', ].join(','); return SliverPinnedHeader( child: Transform.translate( diff --git a/lib/ui/pages/single_season.dart b/lib/ui/pages/single_season.dart index f94f01b..f8cc10c 100644 --- a/lib/ui/pages/single_season.dart +++ b/lib/ui/pages/single_season.dart @@ -13,7 +13,7 @@ import '../../providers/op_model.dart'; import '../../providers/season_model.dart'; import '../../topvars.dart'; import '../../widget/sliver_pinned_header.dart'; -import '../fragments/bangumi_sliver_grid.dart'; +import '../fragments/sliver_bangumi_list.dart'; @FFRoute(name: '/season') class SingleSeasonPage extends StatelessWidget { @@ -47,7 +47,7 @@ class SingleSeasonPage extends StatelessWidget { pushPinnedChildren: true, children: [ _buildWeekSection(theme, bangumiRow), - BangumiSliverGridFragment( + SliverBangumiList( bangumis: bangumiRow.bangumis, handleSubscribe: (bangumi, flag) { context.read().subscribeBangumi( @@ -89,14 +89,14 @@ class SingleSeasonPage extends StatelessWidget { if (bangumiRow.subscribedUpdatedNum > 0) '💖 ${bangumiRow.subscribedUpdatedNum}部', if (bangumiRow.subscribedNum > 0) '❤ ${bangumiRow.subscribedNum}部', - '🎬 ${bangumiRow.num}部' + '🎬 ${bangumiRow.num}部', ].join(','); final full = [ if (bangumiRow.updatedNum > 0) '更新${bangumiRow.updatedNum}部', if (bangumiRow.subscribedUpdatedNum > 0) '订阅更新${bangumiRow.subscribedUpdatedNum}部', if (bangumiRow.subscribedNum > 0) '订阅${bangumiRow.subscribedNum}部', - '共${bangumiRow.num}部' + '共${bangumiRow.num}部', ].join(','); return SliverPinnedHeader( diff --git a/lib/ui/pages/splash.dart b/lib/ui/pages/splash.dart index 69f17eb..b4c145e 100644 --- a/lib/ui/pages/splash.dart +++ b/lib/ui/pages/splash.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import '../../internal/extension.dart'; import '../../internal/kit.dart'; import '../../mikan_routes.dart'; +import '../../res/assets.gen.dart'; import '../../topvars.dart'; import '../fragments/bangumi_cover_scroll_list.dart'; @@ -62,27 +63,24 @@ class _SplashPageState extends State { label: Row( mainAxisSize: MainAxisSize.min, children: [ - Image.asset( - 'assets/mikan.png', - width: 42.0, - isAntiAlias: true, - ), + Assets.mikan.image(width: 42.0), sizedBoxW12, Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Mikan Project', - style: theme.textTheme.titleMedium, + '马上进入', + style: theme.textTheme.titleMedium!.copyWith(height: 1.24), ), Text( - '点我进入', - style: theme.textTheme.bodySmall, + 'Mikan Project', + style: theme.textTheme.bodySmall!.copyWith(height: 1.25), ), ], ), ], ), + extendedPadding: const EdgeInsets.symmetric(horizontal: 12.0), ); } diff --git a/lib/ui/pages/subgroup.dart b/lib/ui/pages/subgroup.dart index f998ebe..c19bb78 100644 --- a/lib/ui/pages/subgroup.dart +++ b/lib/ui/pages/subgroup.dart @@ -13,7 +13,7 @@ import '../../providers/op_model.dart'; import '../../providers/subgroup_model.dart'; import '../../topvars.dart'; import '../../widget/sliver_pinned_header.dart'; -import '../fragments/bangumi_sliver_grid.dart'; +import '../fragments/sliver_bangumi_list.dart'; @FFRoute(name: '/subgroup') @immutable @@ -71,7 +71,7 @@ class SubgroupPage extends StatelessWidget { pushPinnedChildren: true, children: [ _buildYearSeasonSection(theme, gallery.title), - BangumiSliverGridFragment( + SliverBangumiList( flag: gallery.title, bangumis: gallery.bangumis, handleSubscribe: (bangumi, flag) { diff --git a/lib/ui/pages/subscribed_season.dart b/lib/ui/pages/subscribed_season.dart index de8ad24..9079c4d 100644 --- a/lib/ui/pages/subscribed_season.dart +++ b/lib/ui/pages/subscribed_season.dart @@ -15,10 +15,11 @@ import '../../model/season_gallery.dart'; import '../../model/year_season.dart'; import '../../providers/op_model.dart'; import '../../providers/subscribed_season_model.dart'; +import '../../res/assets.gen.dart'; import '../../topvars.dart'; import '../../widget/scalable_tap.dart'; import '../../widget/sliver_pinned_header.dart'; -import '../fragments/bangumi_sliver_grid.dart'; +import '../fragments/sliver_bangumi_list.dart'; @FFRoute(name: '/subscribed/season') @immutable @@ -89,7 +90,7 @@ class SubscribedSeasonPage extends StatelessWidget { if (gallery.bangumis.isNullOrEmpty) _buildEmptySubscribedContainer(theme) else - BangumiSliverGridFragment( + SliverBangumiList( flag: gallery.title, bangumis: gallery.bangumis, handleSubscribe: (bangumi, flag) { @@ -125,10 +126,7 @@ class SubscribedSeasonPage extends StatelessWidget { child: Center( child: Column( children: [ - Image.asset( - 'assets/mikan.png', - width: 64.0, - ), + Assets.mikan.image(width: 64.0), sizedBoxH12, Text( '>_< 您还没有订阅当前季度番组,快去添加订阅吧', diff --git a/lib/widget/bottom_sheet.dart b/lib/widget/bottom_sheet.dart index 28c74e0..4cb15aa 100644 --- a/lib/widget/bottom_sheet.dart +++ b/lib/widget/bottom_sheet.dart @@ -19,15 +19,20 @@ class MBottomSheet extends StatelessWidget { BuildContext context, WidgetBuilder builder, { Color? barrierColor, + bool isScrollControlled = true, + bool enableDrag = true, + bool isDismissible = true, }) { return showModalBottomSheet( context: context, - isScrollControlled: true, - enableDrag: true, - isDismissible: true, + isScrollControlled: isScrollControlled, + enableDrag: enableDrag, + isDismissible: isDismissible, barrierColor: barrierColor, - backgroundColor: Colors.transparent, + backgroundColor: + Theme.of(context).colorScheme.primaryContainer.withOpacity(0.12), builder: builder, + elevation: 0.0, ); } diff --git a/lib/widget/infinite_carousel.dart b/lib/widget/infinite_carousel.dart deleted file mode 100644 index 71760c8..0000000 --- a/lib/widget/infinite_carousel.dart +++ /dev/null @@ -1,650 +0,0 @@ -import 'dart:math' as math; - -import 'package:flutter/physics.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; - -// Default duration and Curve for animateToItem, nextPage and previousPage. -const Duration _kDefaultDuration = Duration(milliseconds: 300); -const Curve _kDefaultCurve = Curves.ease; - -/// `Infinite Carousel` -/// -/// Based on [ListWheelScrollView] to create smooth scroll effect and physics. -class InfiniteCarousel extends StatefulWidget { - /// `Infinite Carousel` - /// - /// Based on [ListWheelScrollView] to create smooth scroll effect and physics. - InfiniteCarousel.builder({ - super.key, - required this.itemCount, - required this.itemExtent, - required this.itemBuilder, - this.physics, - this.controller, - this.onIndexChanged, - this.anchor = 0.0, - this.loop = true, - this.velocityFactor = 0.2, - this.axisDirection = Axis.horizontal, - this.center = true, - this.scrollBehavior, - }) : assert(itemExtent > 0), - assert(itemCount > 0), - assert(velocityFactor > 0.0 && velocityFactor <= 1.0), - childDelegate = SliverChildBuilderDelegate( - (context, index) => - itemBuilder(context, index.abs() % itemCount, index), - childCount: loop ? null : itemCount, - ), - reversedChildDelegate = loop - ? SliverChildBuilderDelegate( - (context, index) => itemBuilder( - context, - itemCount - (index.abs() % itemCount) - 1, - -(index + 1), - ), - ) - : null; - - /// Total items to build for the carousel. - final int itemCount; - - /// Maximum width for single item in viewport. - final double itemExtent; - - /// To lazily build items on the viewport. - /// - /// When Loop: false, ItemIndex is equal to RealIndex (i.e, index of element). - /// - /// When loop: true, two indexes are exposed by item builder. - /// - /// One is `itemIndex`, that is the modded item index i.e., for list of 10, position(11) = 1, and position(-1) = 9. - /// - /// Other is `realIndex`, that is the actual index, i.e. [..., -2, -1, 0, 1, 2, ...] in loop. - /// Real Index is needed if you want to support JumpToItem by tapping on it. - final Widget Function(BuildContext context, int itemIndex, int realIndex) - itemBuilder; - - /// Delegate to lazily build items in forward direction. - final SliverChildDelegate? childDelegate; - - /// Delegate to lazily build items in reverse direction. - final SliverChildDelegate? reversedChildDelegate; - - /// Physics for [InfiniteCarousel]. Defaults to [InfiniteScrollPhysics], which makes sure we always land on a - /// particular item after scrolling. - final ScrollPhysics? physics; - - /// Scroll behavior for [InfiniteCarousel]. - final ScrollBehavior? scrollBehavior; - - /// Scroll controller for [InfiniteScrollPhysics]. - final ScrollController? controller; - - /// Callback fired when item is changed. - final void Function(int)? onIndexChanged; - - /// Where to place selected item on the viewport. Ranges from 0 to 1. - /// - /// 0.0 means selected item is aligned to start of the viewport, and - /// 1.0 meaning selected item is aligned to end of the viewport. - /// Defaults to 0.0. - /// - /// This property is ignored when center is set to true. - final double anchor; - - /// Weather to create a infinite looping list. Defaults to true. - final bool loop; - - /// Axis direction of carousel. Defaults to `horizontal`. - final Axis axisDirection; - - /// Multiply velocity of carousel scrolling by this factor. Defaults to 0.2. - final double velocityFactor; - - /// Align selected item to center of the viewport. When this is true, anchor property is ignored. - final bool center; - - @override - InfiniteCarouselState createState() => InfiniteCarouselState(); -} - -class InfiniteCarouselState extends State { - final Key _forwardListKey = const ValueKey('infinite_carousel_key'); - late InfiniteScrollController scrollController; - late int _lastReportedItemIndex; - - @override - void initState() { - super.initState(); - if (widget.controller != null) { - scrollController = widget.controller as InfiniteScrollController; - } else { - scrollController = InfiniteScrollController(); - } - _lastReportedItemIndex = scrollController.initialItem; - } - - List _buildSlivers() { - final Widget forward = SliverFixedExtentList( - key: _forwardListKey, - delegate: widget.childDelegate!, - itemExtent: widget.itemExtent, - ); - - if (!widget.loop) { - return [forward]; - } - - final Widget reversed = SliverFixedExtentList( - delegate: widget.reversedChildDelegate!, - itemExtent: widget.itemExtent, - ); - return [reversed, forward]; - } - - AxisDirection _getDirection(BuildContext context) { - switch (widget.axisDirection) { - case Axis.horizontal: - assert(debugCheckHasDirectionality(context)); - final TextDirection textDirection = Directionality.of(context); - final AxisDirection axisDirection = - textDirectionToAxisDirection(textDirection); - return axisDirection; - case Axis.vertical: - return AxisDirection.down; - } - } - - @override - Widget build(BuildContext context) { - final AxisDirection axisDirection = _getDirection(context); - - return NotificationListener( - onNotification: (ScrollUpdateNotification notification) { - if (widget.onIndexChanged != null) { - final InfiniteExtentMetrics metrics = - notification.metrics as InfiniteExtentMetrics; - final int currentItem = metrics.itemIndex; - if (currentItem != _lastReportedItemIndex) { - _lastReportedItemIndex = currentItem; - final int trueIndex = - _getTrueIndex(_lastReportedItemIndex, widget.itemCount); - if (widget.onIndexChanged != null) { - widget.onIndexChanged!.call(trueIndex); - } - } - } - return false; - }, - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - final centeredAnchor = _getCenteredAnchor(constraints); - - return _InfiniteScrollable( - controller: scrollController, - itemExtent: widget.itemExtent, - loop: widget.loop, - velocityFactor: widget.velocityFactor, - itemCount: widget.itemCount, - physics: widget.physics ?? const InfiniteScrollPhysics(), - axisDirection: axisDirection, - scrollBehavior: widget.scrollBehavior ?? - ScrollConfiguration.of(context).copyWith(scrollbars: false), - viewportBuilder: (BuildContext context, ViewportOffset position) { - return Viewport( - center: _forwardListKey, - anchor: centeredAnchor, - axisDirection: axisDirection, - offset: position, - slivers: _buildSlivers(), - ); - }, - ); - }, - ), - ); - } - - // Get anchor for viewport to place the item in exact center. - double _getCenteredAnchor(BoxConstraints constraints) { - if (!widget.center) { - return widget.anchor; - } - - final total = widget.axisDirection == Axis.horizontal - ? constraints.maxWidth - : constraints.maxHeight; - return ((total / 2) - (widget.itemExtent / 2)) / total; - } -} - -/// Extend Scrollable to also include viewport children's itemExtent, itemCount, loop and other values. -/// This is done so that ScrollPosition and Physics can also access these values via scroll context. -class _InfiniteScrollable extends Scrollable { - const _InfiniteScrollable({ - super.axisDirection = AxisDirection.right, - super.controller, - super.physics, - super.scrollBehavior, - required this.itemExtent, - required this.itemCount, - required this.loop, - required this.velocityFactor, - required super.viewportBuilder, - }); - - final double itemExtent; - final int itemCount; - final bool loop; - final double velocityFactor; - - @override - _InfiniteScrollableState createState() => _InfiniteScrollableState(); -} - -class _InfiniteScrollableState extends ScrollableState { - double get itemExtent => (widget as _InfiniteScrollable).itemExtent; - - int get itemCount => (widget as _InfiniteScrollable).itemCount; - - bool get loop => (widget as _InfiniteScrollable).loop; - - double get velocityFactor => (widget as _InfiniteScrollable).velocityFactor; -} - -/// Scroll controller for [InfiniteCarousel]. -class InfiniteScrollController extends ScrollController { - /// Scroll controller for [InfiniteCarousel]. - InfiniteScrollController({this.initialItem = 0}); - - /// Initial item index for [InfiniteScrollController]. Defaults to 0. - final int initialItem; - - /// Returns selected Item index. If loop => true, then it returns the modded index value. - int get selectedItem => _getTrueIndex( - (position as _InfiniteScrollPosition).itemIndex, - (position as _InfiniteScrollPosition).itemCount, - ); - - /// Animate to specific item index. - Future animateToItem( - int itemIndex, { - Duration duration = _kDefaultDuration, - Curve curve = _kDefaultCurve, - }) async { - if (!hasClients) { - return; - } - - await Future.wait([ - for (final position in positions.cast<_InfiniteScrollPosition>()) - position.animateTo( - itemIndex * position.itemExtent, - duration: duration, - curve: curve, - ), - ]); - } - - /// Jump to specific item index. - void jumpToItem(int itemIndex) { - for (final position in positions.cast<_InfiniteScrollPosition>()) { - position.jumpTo(itemIndex * position.itemExtent); - } - } - - /// Animate to next item in viewport. - Future nextItem({ - Duration duration = _kDefaultDuration, - Curve curve = _kDefaultCurve, - }) async { - if (!hasClients) { - return; - } - - await Future.wait([ - for (final position in positions.cast<_InfiniteScrollPosition>()) - position.animateTo( - offset + position.itemExtent, - duration: duration, - curve: curve, - ), - ]); - } - - /// Animate to previous item in viewport. - Future previousItem({ - Duration duration = _kDefaultDuration, - Curve curve = _kDefaultCurve, - }) async { - if (!hasClients) { - return; - } - - await Future.wait([ - for (final position in positions.cast<_InfiniteScrollPosition>()) - position.animateTo( - offset - position.itemExtent, - duration: duration, - curve: curve, - ), - ]); - } - - @override - ScrollPosition createScrollPosition( - ScrollPhysics physics, - ScrollContext context, - ScrollPosition? oldPosition, - ) { - return _InfiniteScrollPosition( - physics: physics, - context: context, - initialItem: initialItem, - oldPosition: oldPosition, - ); - } -} - -/// Metrics for Infinite scroll controller. This is an immutable snapshot of the current values of scroll position. -/// This can directly be accessed by ScrollNotification to currently selected real item index at any time. -class InfiniteExtentMetrics extends FixedScrollMetrics { - InfiniteExtentMetrics({ - required super.minScrollExtent, - required super.maxScrollExtent, - required super.pixels, - required super.viewportDimension, - required super.devicePixelRatio, - required super.axisDirection, - required this.itemIndex, - }); - - @override - InfiniteExtentMetrics copyWith({ - double? minScrollExtent, - double? maxScrollExtent, - double? pixels, - double? viewportDimension, - double? devicePixelRatio, - AxisDirection? axisDirection, - int? itemIndex, - }) { - return InfiniteExtentMetrics( - // 解决 Null check operator used on a null value - minScrollExtent: minScrollExtent ?? - (hasContentDimensions ? this.minScrollExtent : 0.0), - maxScrollExtent: maxScrollExtent ?? this.maxScrollExtent, - pixels: pixels ?? this.pixels, - viewportDimension: viewportDimension ?? this.viewportDimension, - devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio, - axisDirection: axisDirection ?? this.axisDirection, - itemIndex: itemIndex ?? this.itemIndex, - ); - } - - /// The scroll view's currently selected item index. - final int itemIndex; -} - -int _getItemFromOffset({ - required double offset, - required double itemExtent, - required double minScrollExtent, - required double maxScrollExtent, -}) { - return (_clipOffsetToScrollableRange( - offset, - minScrollExtent, - maxScrollExtent, - ) / - itemExtent) - .round(); -} - -double _clipOffsetToScrollableRange( - double offset, - double minScrollExtent, - double maxScrollExtent, -) { - return math.min(math.max(offset, minScrollExtent), maxScrollExtent); -} - -/// Get the modded item index from real index. -int _getTrueIndex(int currentIndex, int totalCount) { - if (currentIndex >= 0) { - return currentIndex % totalCount; - } - - return (totalCount + (currentIndex % totalCount)) % totalCount; -} - -class _InfiniteScrollPosition extends ScrollPositionWithSingleContext - implements InfiniteExtentMetrics { - _InfiniteScrollPosition({ - required super.physics, - required super.context, - required int initialItem, - super.oldPosition, - }) : assert(context is _InfiniteScrollableState), - super( - initialPixels: _getItemExtentFromScrollContext(context) * initialItem, - ); - - double get itemExtent => _getItemExtentFromScrollContext(context); - - static double _getItemExtentFromScrollContext(ScrollContext context) { - return (context as _InfiniteScrollableState).itemExtent; - } - - int get itemCount => _getItemCountFromScrollContext(context); - - static int _getItemCountFromScrollContext(ScrollContext context) { - return (context as _InfiniteScrollableState).itemCount; - } - - bool get loop => _getLoopFromScrollContext(context); - - static bool _getLoopFromScrollContext(ScrollContext context) { - return (context as _InfiniteScrollableState).loop; - } - - double get velocityFactor => _getVelocityFactorFromScrollContext(context); - - static double _getVelocityFactorFromScrollContext(ScrollContext context) { - return (context as _InfiniteScrollableState).velocityFactor; - } - - @override - double get maxScrollExtent => loop - ? (super.hasContentDimensions ? super.maxScrollExtent : 0.0) - : itemExtent * (itemCount - 1); - - @override - int get itemIndex { - return _getItemFromOffset( - offset: pixels, - itemExtent: itemExtent, - // 解决 Null check operator used on a null value - minScrollExtent: hasContentDimensions ? minScrollExtent : 0.0, - maxScrollExtent: maxScrollExtent, - ); - } - - @override - InfiniteExtentMetrics copyWith({ - double? minScrollExtent, - double? maxScrollExtent, - double? pixels, - double? viewportDimension, - double? devicePixelRatio, - AxisDirection? axisDirection, - int? itemIndex, - }) { - return InfiniteExtentMetrics( - // 解决 Null check operator used on a null value - minScrollExtent: minScrollExtent ?? - (hasContentDimensions ? this.minScrollExtent : 0.0), - maxScrollExtent: maxScrollExtent ?? this.maxScrollExtent, - pixels: pixels ?? this.pixels, - viewportDimension: viewportDimension ?? this.viewportDimension, - axisDirection: axisDirection ?? this.axisDirection, - itemIndex: itemIndex ?? this.itemIndex, - devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio, - ); - } -} - -/// Physics for [InfiniteCarousel]. -/// -/// Based on Flutter's FixedExtentScrollPhysics. Hence, it always lands on a particular item. -/// -/// If loop => false, friction is applied when user tries to go beyond Viewport area. -/// Friction factor is calculated the way its done in BouncingScrollPhycics. -class InfiniteScrollPhysics extends ScrollPhysics { - const InfiniteScrollPhysics({super.parent}); - - @override - InfiniteScrollPhysics applyTo(ScrollPhysics? ancestor) { - return InfiniteScrollPhysics(parent: buildParent(ancestor)); - } - - @override - double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0; - - /// Increase friction for scrolling in out-of-bound areas. - double frictionFactor(double overscrollFraction) => - 0.12 * math.pow(1 - overscrollFraction, 2); - - @override - double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { - if (position.pixels > position.minScrollExtent && - position.pixels < position.maxScrollExtent) { - return offset; - } - - final double overscrollPastStart = - math.max(position.minScrollExtent - position.pixels, 0.0); - final double overscrollPastEnd = - math.max(position.pixels - position.maxScrollExtent, 0.0); - final double overscrollPast = - math.max(overscrollPastStart, overscrollPastEnd); - final bool easing = (overscrollPastStart > 0.0 && offset < 0.0) || - (overscrollPastEnd > 0.0 && offset > 0.0); - - final double friction = easing - // Apply less resistance when easing the overscroll vs tensioning. - ? frictionFactor( - (overscrollPast - offset.abs()) / position.viewportDimension, - ) - : frictionFactor(overscrollPast / position.viewportDimension); - final double direction = offset.sign; - - return direction * _applyFriction(overscrollPast, offset.abs(), friction); - } - - static double _applyFriction( - double extentOutside, - double absDelta, - double gamma, - ) { - assert(absDelta > 0); - double total = 0.0; - if (extentOutside > 0) { - final double deltaToLimit = extentOutside / gamma; - if (absDelta < deltaToLimit) { - return absDelta * gamma; - } - total += extentOutside; - absDelta -= deltaToLimit; - } - return total + absDelta; - } - - @override - Simulation? createBallisticSimulation( - ScrollMetrics position, - double velocity, - ) { - final _InfiniteScrollPosition metrics = position as _InfiniteScrollPosition; - - // Scenario 1: - // If we're out of range and not headed back in range, defer to the parent - // ballistics, which should put us back in range at the scrollable's boundary. - if ((velocity <= 0.0 && metrics.pixels <= metrics.minScrollExtent) || - (velocity >= 0.0 && metrics.pixels >= metrics.maxScrollExtent)) { - return super.createBallisticSimulation(metrics, velocity); - } - - // Create a test simulation to see where it would have ballistically fallen - // naturally without settling onto items. - final Simulation? testFrictionSimulation = super.createBallisticSimulation( - metrics, - velocity * math.min(metrics.velocityFactor + 0.15, 1.0), - ); - - // Scenario 2: - // If it was going to end up past the scroll extent, defer back to the - // parent physics' ballistics again which should put us on the scrollable's - // boundary. - if (testFrictionSimulation != null && - (testFrictionSimulation.x(double.infinity) == metrics.minScrollExtent || - testFrictionSimulation.x(double.infinity) == - metrics.maxScrollExtent)) { - return super.createBallisticSimulation(metrics, velocity); - } - - // From the natural final position, find the nearest item it should have - // settled to. - final int settlingItemIndex = _getItemFromOffset( - offset: testFrictionSimulation?.x(double.infinity) ?? metrics.pixels, - itemExtent: metrics.itemExtent, - minScrollExtent: metrics.minScrollExtent, - maxScrollExtent: metrics.maxScrollExtent, - ); - - final double settlingPixels = settlingItemIndex * metrics.itemExtent; - final tolerance = toleranceFor( - FixedScrollMetrics( - minScrollExtent: null, - maxScrollExtent: null, - pixels: null, - viewportDimension: null, - axisDirection: AxisDirection.down, - devicePixelRatio: WidgetsBinding.instance.window.devicePixelRatio, - ), - ); - - // Scenario 3: - // If there's no velocity and we're already at where we intend to land, - // do nothing. - if (velocity.abs() < tolerance.velocity && - (settlingPixels - metrics.pixels).abs() < tolerance.distance) { - return null; - } - - // Scenario 4: - // If we're going to end back at the same item because initial velocity - // is too low to break past it, use a spring simulation to get back. - if (settlingItemIndex == metrics.itemIndex) { - return SpringSimulation( - spring, - metrics.pixels, - settlingPixels, - velocity * metrics.velocityFactor, - tolerance: tolerance, - ); - } - - // Scenario 5: - // Create a new friction simulation except the drag will be tweaked to land - // exactly on the item closest to the natural stopping point. - return FrictionSimulation.through( - metrics.pixels, - settlingPixels, - velocity * metrics.velocityFactor, - tolerance.velocity * metrics.velocityFactor * velocity.sign, - ); - } -} diff --git a/lib/widget/loading.dart b/lib/widget/loading.dart new file mode 100644 index 0000000..faada16 --- /dev/null +++ b/lib/widget/loading.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class LoadingWidget extends StatelessWidget { + const LoadingWidget({super.key, required this.msg}); + + final String msg; + + @override + Widget build(BuildContext context) { + return const Center( + child: SizedBox( + width: 36.0, + height: 36.0, + child: CircularProgressIndicator(), + ), + ); + } +} diff --git a/lib/widget/sliver_pinned_header.dart b/lib/widget/sliver_pinned_header.dart index f5c97d0..0410447 100644 --- a/lib/widget/sliver_pinned_header.dart +++ b/lib/widget/sliver_pinned_header.dart @@ -103,6 +103,15 @@ class SliverPinnedAppBar extends StatelessWidget { decoration: BoxDecoration( color: theme.colorScheme.background, borderRadius: borderRadius, + border: offsetRatio > 0.1 + ? Border( + bottom: Divider.createBorderSide( + context, + color: theme.colorScheme.outlineVariant, + width: 0.0, + ), + ) + : const Border(), ), padding: EdgeInsetsDirectional.only( start: lp, diff --git a/lib/widget/toast.dart b/lib/widget/toast.dart new file mode 100644 index 0000000..d42baba --- /dev/null +++ b/lib/widget/toast.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import '../topvars.dart'; + +class ToastWidget extends StatelessWidget { + const ToastWidget({super.key, required this.msg}); + + final String msg; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + decoration: BoxDecoration( + color: theme.colorScheme.tertiaryContainer, + borderRadius: borderRadius12, + ), + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), + child: Text( + msg, + style: TextStyle(color: theme.colorScheme.onTertiaryContainer), + ), + ); + } +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 2314096..1565a7e 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -262,7 +262,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index fa971fe..0a4f0a0 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.0.0" - flutter: ">=3.10.0" + flutter: ">=3.13.0" dependencies: flutter: @@ -20,7 +20,6 @@ dependencies: dio_cookie_manager: ^3.0.0 html: ^0.15.3 provider: ^6.0.5 - oktoast: ^3.3.1 hive: ^2.2.3 hive_flutter: ^1.1.0 clipboard: ^0.1.3 @@ -48,6 +47,8 @@ dependencies: window_manager: ^0.3.2 dynamic_color: ^1.6.5 decimal: ^2.3.2 + flutter_smart_dialog: ^4.9.4 + infinite_carousel: ^1.0.3 dev_dependencies: flutter_test: @@ -55,9 +56,11 @@ dev_dependencies: args: ^2.4.1 yaml: ^3.1.2 glob: ^2.1.1 - http: ^0.13.6 + http: ^1.1.0 process_run: ^0.13.0 build_runner: ^2.3.3 + flutter_gen: ^5.3.1 + flutter_gen_runner: ^5.3.1 json_serializable: ^6.6.2 hive_generator: ^2.0.0 flutter_lints: ^2.0.1 @@ -68,11 +71,15 @@ flutter: uses-material-design: true assets: - assets/ - fonts: - - family: mono - fonts: - - asset: assets/fonts/mono/JetBrainsMono-Regular.ttf - - asset: assets/fonts/mono/JetBrainsMono-Bold.ttf + +flutter_gen: + output: lib/res/ + line_length: 80 + integrations: + flutter_svg: true + flare_flutter: true + rive: true + lottie: true icons_launcher: image_path: 'assets/mikan.png' diff --git a/scripts/github.dart b/scripts/github.dart index 44f469d..96c68ab 100644 --- a/scripts/github.dart +++ b/scripts/github.dart @@ -28,7 +28,7 @@ Future main(List arguments) async { result.first.stdout.toString().trim().split('\n').last.split('/'); final repo = [ urlParts[urlParts.length - 2], - urlParts[urlParts.length - 1].split(' ').first.replaceAll('.git', '') + urlParts[urlParts.length - 1].split(' ').first.replaceAll('.git', ''), ].join('/'); switch (Fun.values.firstWhere((e) => e.name == parse['fun'])) { case Fun.release: @@ -106,7 +106,7 @@ Future _release({ 'body': '', 'draft': false, 'prerelease': false, - 'generate_release_notes': true + 'generate_release_notes': true, }); final response = await http.post( Uri.parse('https://api.github.com/repos/$repo/releases'), diff --git a/scripts/releases.dart b/scripts/releases.dart index b15d40a..9b06205 100644 --- a/scripts/releases.dart +++ b/scripts/releases.dart @@ -20,13 +20,15 @@ Future main() async { 'armeabi-v7a', 'x86_64', 'universal', - 'win32' + 'win32', }; final files = [ ...result['assets'].map((it) { final name = it['name']; - final arch = arches.firstWhere(name.contains, - orElse: () => null,); + final arch = arches.firstWhere( + name.contains, + orElse: () => null, + ); final size = it['size']; final sizefmt = (size / 1024 / 1024).toStringAsFixed(2) + 'MB'; return { @@ -38,7 +40,7 @@ Future main() async { 'cdl': 'https://cdn.jsdelivr.net/gh/iota9star/mikan_flutter@master/releases/$name', }; - }) + }), ]; await Jiffy.setLocale('zh_cn'); final jiffy = Jiffy.parse(result['published_at'])..add(hours: 8); diff --git a/windows_inno_setup.iss b/windows_inno_setup.iss index 8b22e08..7f5eb3c 100644 --- a/windows_inno_setup.iss +++ b/windows_inno_setup.iss @@ -3,7 +3,7 @@ #define MyAppName "MikanProject" #define MyAppEngName "MikanProject" -#define MyAppVersion "1.1.1" +#define MyAppVersion "1.2.0" #define MyAppPublisher "mikanani.me" #define MyAppURL "https://mikanani.me/" #define MyAppExeName "mikan.exe"