diff --git a/android/app/src/main/res/mipmap-xhdpi/launch_image_music.png b/android/app/src/main/res/mipmap-xhdpi/launch_image_music.png deleted file mode 100755 index f96f596..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/launch_image_music.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launch_image_music.png b/android/app/src/main/res/mipmap-xxhdpi/launch_image_music.png deleted file mode 100755 index ac23d3d..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/launch_image_music.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launch_image_music.png b/android/app/src/main/res/mipmap-xxxhdpi/launch_image_music.png deleted file mode 100755 index 6780144..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/launch_image_music.png and /dev/null differ diff --git a/assets/images/side_b/album_title_bg.png b/assets/images/side_b/album_title_bg.png new file mode 100755 index 0000000..fc56ec5 Binary files /dev/null and b/assets/images/side_b/album_title_bg.png differ diff --git a/assets/images/side_b/album_total.png b/assets/images/side_b/album_total.png new file mode 100644 index 0000000..89fb054 Binary files /dev/null and b/assets/images/side_b/album_total.png differ diff --git a/assets/images/side_b/artists.png b/assets/images/side_b/artists.png new file mode 100644 index 0000000..9c34a96 Binary files /dev/null and b/assets/images/side_b/artists.png differ diff --git a/assets/images/side_b/bottom_sheet_indicator.png b/assets/images/side_b/bottom_sheet_indicator.png new file mode 100644 index 0000000..e6205cc Binary files /dev/null and b/assets/images/side_b/bottom_sheet_indicator.png differ diff --git a/assets/images/side_b/collection_album.png b/assets/images/side_b/collection_album.png new file mode 100644 index 0000000..0fb8b43 Binary files /dev/null and b/assets/images/side_b/collection_album.png differ diff --git a/assets/images/side_b/launch_image.png b/assets/images/side_b/launch_image.png deleted file mode 100755 index 6780144..0000000 Binary files a/assets/images/side_b/launch_image.png and /dev/null differ diff --git a/assets/images/side_b/love_solid.png b/assets/images/side_b/love_solid.png new file mode 100644 index 0000000..e86ee47 Binary files /dev/null and b/assets/images/side_b/love_solid.png differ diff --git a/assets/images/side_b/next_song.png b/assets/images/side_b/next_track.png similarity index 100% rename from assets/images/side_b/next_song.png rename to assets/images/side_b/next_track.png diff --git a/assets/images/side_b/not_collection_album.png b/assets/images/side_b/not_collection_album.png new file mode 100644 index 0000000..f0dfe5e Binary files /dev/null and b/assets/images/side_b/not_collection_album.png differ diff --git a/assets/images/side_b/offline_download.png b/assets/images/side_b/offline_download.png new file mode 100644 index 0000000..7e3afbb Binary files /dev/null and b/assets/images/side_b/offline_download.png differ diff --git a/assets/images/side_b/personal_music_library_bg.png b/assets/images/side_b/personal_music_library_bg.png new file mode 100644 index 0000000..fbbab66 Binary files /dev/null and b/assets/images/side_b/personal_music_library_bg.png differ diff --git a/assets/images/side_b/placeholder_library.png b/assets/images/side_b/placeholder_library.png new file mode 100644 index 0000000..4ef1042 Binary files /dev/null and b/assets/images/side_b/placeholder_library.png differ diff --git a/assets/images/side_b/play_list_delete.png b/assets/images/side_b/play_list_delete.png new file mode 100644 index 0000000..5cd8e3f Binary files /dev/null and b/assets/images/side_b/play_list_delete.png differ diff --git a/assets/images/side_b/playlists_add.png b/assets/images/side_b/playlists_add.png new file mode 100644 index 0000000..0220b77 Binary files /dev/null and b/assets/images/side_b/playlists_add.png differ diff --git a/assets/images/side_b/previous_song.png b/assets/images/side_b/previous_track.png similarity index 100% rename from assets/images/side_b/previous_song.png rename to assets/images/side_b/previous_track.png diff --git a/lib/ads/app_open_ad_manager.dart b/lib/ads/app_open_ad_manager.dart index 8c5c657..90df786 100644 --- a/lib/ads/app_open_ad_manager.dart +++ b/lib/ads/app_open_ad_manager.dart @@ -2,14 +2,12 @@ // Date: 2024/6/25 // Description: 开屏广告 +import 'dart:io' show Platform; + import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; -import 'package:tone_snap/routes/app_routes.dart'; -import 'dart:io' show Platform; - import 'package:tone_snap/utils/log_util.dart'; class AppOpenAdManager { @@ -66,16 +64,16 @@ class AppOpenAdManager { /// 显示广告(如果存在且尚未显示) /// 如果先前缓存的广告已过期,则只加载并缓存新广告 - void showAdIfAvailable() { + void showAdIfAvailable({Function()? onTap}) { if (!isAdAvailable) { LogUtil.d('尝试在可用之前显示广告'); loadAd(); - - _openInitial(); + if(onTap != null) onTap(); return; } if (_isShowingAd) { LogUtil.d('尝试在已显示广告的情况下显示广告'); + if(onTap != null) onTap(); return; } if (DateTime.now().subtract(maxCacheDuration).isAfter(_appOpenLoadTime!)) { @@ -83,8 +81,7 @@ class AppOpenAdManager { _appOpenAd!.dispose(); _appOpenAd = null; loadAd(); - - _openInitial(); + if(onTap != null) onTap(); return; } // 设置 fullScreenContentCallback 并显示广告 @@ -107,7 +104,7 @@ class AppOpenAdManager { ad.dispose(); _appOpenAd = null; - _openInitial(); + if(onTap != null) onTap(); }, onAdDismissedFullScreenContent: (ad) { LogUtil.d('$ad onAdDismissedFullScreenContent'); @@ -119,7 +116,7 @@ class AppOpenAdManager { _appOpenAd = null; loadAd(); - _openInitial(); + if(onTap != null) onTap(); }, onAdClicked: (ad) { LogUtil.d('$ad onAdClicked'); @@ -127,9 +124,4 @@ class AppOpenAdManager { ); _appOpenAd!.show(); } - - /// 打开首个页面 - void _openInitial() { - Get.offNamed(AppRoutes.initialA); - } } \ No newline at end of file diff --git a/lib/ads/interstitial_ad_manager.dart b/lib/ads/interstitial_ad_manager.dart index cab6f07..ec4cef3 100644 --- a/lib/ads/interstitial_ad_manager.dart +++ b/lib/ads/interstitial_ad_manager.dart @@ -62,14 +62,16 @@ class InterstitialAdManager { /// 显示广告(如果存在且尚未显示) /// 如果先前缓存的广告已过期,则只加载并缓存新广告 - void showAdIfAvailable() { + void showAdIfAvailable({Function()? onTap}) { if (!isAdAvailable) { LogUtil.d('尝试在可用之前显示广告'); loadAd(); + if(onTap != null) onTap(); return; } if (isShowingAd) { LogUtil.d('尝试在已显示广告的情况下显示广告'); + if(onTap != null) onTap(); return; } if (DateTime.now().subtract(maxCacheDuration).isAfter(_appOpenLoadTime!)) { @@ -77,6 +79,7 @@ class InterstitialAdManager { _interstitialAd!.dispose(); _interstitialAd = null; loadAd(); + if(onTap != null) onTap(); return; } // 设置 fullScreenContentCallback 并显示广告 @@ -95,6 +98,8 @@ class InterstitialAdManager { isShowingAd = false; ad.dispose(); _interstitialAd = null; + + if(onTap != null) onTap(); }, onAdDismissedFullScreenContent: (ad) { LogUtil.d('$ad onAdDismissedFullScreenContent'); @@ -102,6 +107,8 @@ class InterstitialAdManager { ad.dispose(); _interstitialAd = null; loadAd(); + + if(onTap != null) onTap(); }, onAdClicked: (ad) { LogUtil.d('$ad onAdClicked'); diff --git a/lib/components/base_easyloading.dart b/lib/components/base_easyloading.dart index 7831fb1..9609730 100644 --- a/lib/components/base_easyloading.dart +++ b/lib/components/base_easyloading.dart @@ -2,11 +2,24 @@ // Date: 2024/5/7 // Description: 加载、进度、提示框 +import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; class BaseEasyLoading { - static void initGlobalConfig() { + static void configLoading() { EasyLoading.instance + // 自定义加载样式 + ..loadingStyle = EasyLoadingStyle.custom + ..indicatorType = EasyLoadingIndicatorType.ring + ..indicatorSize = 30 + ..lineWidth = 3 + ..textColor = seedColor + ..indicatorColor = seedColor + ..progressColor = seedColor + ..backgroundColor = scaffoldBgColor.withOpacity(0.8) + ..maskColor = Colors.amber + ..boxShadow = [] // 当loading展示的时候,是否允许用户操作. ..userInteractions = false // 点击背景是否关闭. @@ -30,7 +43,7 @@ class BaseEasyLoading { bool show = true, }) { EasyLoading.instance.userInteractions = false; - if (show) EasyLoading.show(status: value, dismissOnTap: dismissOnTap); + if (show) EasyLoading.show(status: value, dismissOnTap: true); } static void dismiss({bool dismiss = true}) { diff --git a/lib/components/base_scrollbar.dart b/lib/components/base_scrollbar.dart index 7a4bf5e..d81c18e 100644 --- a/lib/components/base_scrollbar.dart +++ b/lib/components/base_scrollbar.dart @@ -3,21 +3,31 @@ // Description: 列表滚动条 import 'package:flutter/material.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; class BaseScrollbar extends StatelessWidget { - const BaseScrollbar({super.key, required this.child}); + const BaseScrollbar({ + super.key, + required this.child, + this.scrollController, + this.thumbColor, + }); final Widget child; + final Color? thumbColor; + final ScrollController? scrollController; @override Widget build(BuildContext context) { return MediaQuery.removePadding( context: context, removeTop: true, + removeBottom: true, child: RawScrollbar( - thumbColor: Colors.white, + controller: scrollController, + thumbColor: thumbColor ?? seedColor, radius: const Radius.circular(8.0), - thickness: 4.0, + thickness: 2.0, thumbVisibility: false, child: child, ), diff --git a/lib/components/music_bar.dart b/lib/components/music_bar.dart new file mode 100644 index 0000000..d70ba64 --- /dev/null +++ b/lib/components/music_bar.dart @@ -0,0 +1,34 @@ +// Author: fengshengxiong +// Date: 2024/6/30 +// Description: 全局音乐播放栏 + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/music_bar/music_bar_view.dart'; + +class MusicBar { + static final MusicBar _instance = MusicBar._(); + factory MusicBar() => _instance; + MusicBar._(); + + OverlayEntry? overlayEntry; + var isShow = false.obs; + + /// 显示音乐栏 + show() { + if (overlayEntry == null && Get.context != null) { + overlayEntry = OverlayEntry(builder: (BuildContext context) { + return MusicBarView(); + }); + Overlay.of(Get.context!).insert(overlayEntry!); + isShow.value = true; + } + } + + /// 隐藏音乐栏 + void hide() { + overlayEntry?.remove(); + overlayEntry = null; + isShow.value = false; + } +} diff --git a/lib/components/music_bar/music_bar_binding.dart b/lib/components/music_bar/music_bar_binding.dart new file mode 100644 index 0000000..3492355 --- /dev/null +++ b/lib/components/music_bar/music_bar_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'music_bar_controller.dart'; + +class MusicBarBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => MusicBarController()); + } +} diff --git a/lib/components/music_bar/music_bar_controller.dart b/lib/components/music_bar/music_bar_controller.dart new file mode 100644 index 0000000..90f90ba --- /dev/null +++ b/lib/components/music_bar/music_bar_controller.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class MusicBarController extends GetxController with GetSingleTickerProviderStateMixin { + static MusicBarController get to => Get.find(); + AnimationController? _controller; + var bottom = 0.0.obs; + double bottomPadding = 0.0; + + @override + void onInit() { + super.onInit(); + _controller = AnimationController(vsync: this)..duration = const Duration(milliseconds: 200); + WidgetsBinding.instance.addPostFrameCallback((_) { + bottomPadding = MediaQuery.of(Get.context!).padding.bottom; + bottom.value = kBottomNavigationBarHeight + bottomPadding; + }); + } + + @override + void onClose() { + _controller?.dispose(); + super.onClose(); + } + + /// 底部导航栏消失时沉底 + void toBottom() { + var animation = Tween(begin: bottom.value, end: bottomPadding).animate(_controller!); + animation.addListener(() { + bottom.value = animation.value; + }); + _controller!.forward(); + } + + /// 底部导航栏出现时抬高 + void riseUp() { + var animation = Tween(begin: bottom.value, end: kBottomNavigationBarHeight + bottomPadding).animate(_controller!); + animation.addListener(() { + bottom.value = animation.value; + }); + _controller!.forward(); + } + + /// 打开播放页面 + void openPlayPage() { + Get.toNamed(AppRoutes.playPage, arguments: {'isMusicBarOpen': true}); + } +} diff --git a/lib/components/music_bar/music_bar_view.dart b/lib/components/music_bar/music_bar_view.dart new file mode 100644 index 0000000..662fff5 --- /dev/null +++ b/lib/components/music_bar/music_bar_view.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/my_marquee_text.dart'; +import 'package:tone_snap/components/network_image_widget.dart'; +import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart'; +import 'package:tone_snap/res/themes/app_sizes.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +import 'music_bar_controller.dart'; + +class MusicBarView extends StatelessWidget { + MusicBarView({super.key}); + + final controller = Get.put(MusicBarController(), permanent: true); + final musicPlayerController = MusicPlayerController.to; + + @override + Widget build(BuildContext context) { + return Obx(() { + return Stack( + fit: StackFit.expand, + children: [ + Positioned( + bottom: controller.bottom.value, + left: 16.w, + right: 16.w, + child: GestureDetector( + onTap: controller.openPlayPage, + child: Container( + width: 1.sw - 32.w, + height: musicBarHeight, + padding: EdgeInsets.symmetric(horizontal: 26.w), + decoration: BoxDecoration( + color: const Color(0xFF80F988), + borderRadius: BorderRadius.circular(36).r, + boxShadow: const [ + BoxShadow( + color: Color(0x40040604), + offset: Offset(0, 4), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + child: Row( + children: [ + ClipOval( + child: Obx(() { + return NetworkImageWidget( + url: musicPlayerController.musicModel.value.thumbnail, + width: 48.w, + height: 48.w, + ); + }), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + return MyMarqueeText( + text: ObjUtil.getStr(musicPlayerController.musicModel.value.title), + textStyle: TextStyle( + color: Colors.black, + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ); + }), + SizedBox(height: 4.h), + Obx(() { + return MyMarqueeText( + text: ObjUtil.getStr(musicPlayerController.musicModel.value.subTitle), + textStyle: TextStyle( + color: Colors.black, + fontSize: 12.sp, + ), + ); + }), + ], + ), + ), + ], + ), + ), + ), + ), + ], + ); + }); + } +} diff --git a/lib/components/my_marquee_text.dart b/lib/components/my_marquee_text.dart index 0b42e69..57df683 100644 --- a/lib/components/my_marquee_text.dart +++ b/lib/components/my_marquee_text.dart @@ -6,19 +6,25 @@ import 'package:flutter/cupertino.dart'; import 'package:widget_marquee/widget_marquee.dart'; class MyMarqueeText extends StatelessWidget { - const MyMarqueeText({super.key, required this.text, required this.textStyle}); + const MyMarqueeText({ + super.key, + required this.text, + required this.textStyle, + this.enable = true, + }); final String text; final TextStyle textStyle; + final bool enable; @override Widget build(BuildContext context) { return Marquee( delay: const Duration(seconds: 2), - duration: const Duration(seconds: 6), + duration: Duration(seconds: enable ? 12 : 0), pause: Duration.zero, - gap: 60, + gap: 80, child: Text(text, style: textStyle), ); } -} \ No newline at end of file +} diff --git a/lib/components/network_image_widget.dart b/lib/components/network_image_widget.dart index d9905db..32b7a73 100644 --- a/lib/components/network_image_widget.dart +++ b/lib/components/network_image_widget.dart @@ -12,10 +12,10 @@ class NetworkImageWidget extends StatelessWidget { this.width, this.height, this.radius = 0.0, - required this.url, + this.url, this.fit = BoxFit.cover, this.placeholder, - this.errorWidget, + this.noPlaceholder = false, }); final double? width; @@ -23,8 +23,8 @@ class NetworkImageWidget extends StatelessWidget { final double radius; final String? url; final BoxFit fit; - final Widget? placeholder; - final Widget? errorWidget; + final String? placeholder; + final bool noPlaceholder; @override Widget build(BuildContext context) { @@ -35,23 +35,20 @@ class NetworkImageWidget extends StatelessWidget { height: height, imageUrl: '$url', fit: fit, - placeholder: (context, url) { - return placeholder ?? _placeholderWidget(Assets.sideBImgPlaceholder); + placeholder: noPlaceholder ? null : (context, url) { + return _placeholderWidget(Assets.sideBImgPlaceholder); }, - errorWidget: (context, url, error) { - return errorWidget ?? _placeholderWidget(Assets.sideBImgError); + errorWidget: noPlaceholder ? null : (context, url, error) { + return _placeholderWidget(Assets.sideBImgError); }, ), ); } - Widget _placeholderWidget(String imgName) { - return Container( - color: Colors.white10, - child: Image.asset( - imgName, - color: Colors.white12, - ), + Widget _placeholderWidget(String img) { + return Image.asset( + placeholder ?? img, + color: Colors.white12, ); } } diff --git a/lib/components/shader_mask.dart b/lib/components/shader_mask.dart new file mode 100644 index 0000000..a9fbaa8 --- /dev/null +++ b/lib/components/shader_mask.dart @@ -0,0 +1,25 @@ +// Author: fengshengxiong +// Date: 2024/6/30 +// Description: ShaderMaskWidget + +import 'package:flutter/material.dart'; + +class ShaderMaskWidget extends StatelessWidget { + const ShaderMaskWidget({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return ShaderMask( + shaderCallback: (Rect bounds) { + return const LinearGradient( + colors: [Colors.transparent, Colors.white, Colors.white, Colors.transparent], + stops: [0.0, 0.05, 0.95, 1], + ).createShader(bounds); + }, + blendMode: BlendMode.dstIn, + child: child, + ); + } +} diff --git a/lib/components/view_state_widget.dart b/lib/components/view_state_widget.dart index 06a492a..afb0a53 100644 --- a/lib/components/view_state_widget.dart +++ b/lib/components/view_state_widget.dart @@ -13,10 +13,12 @@ class ViewStateWidget extends StatelessWidget { super.key, required this.viewState, required this.child, + this.cpiBgColor, }); final ViewState viewState; final Widget child; + final Color? cpiBgColor; @override Widget build(BuildContext context) { @@ -24,7 +26,9 @@ class ViewStateWidget extends StatelessWidget { case ViewState.normal: return child; case ViewState.loading: - return loadingView(); + return loadingView( + backgroundColor: cpiBgColor, + ); case ViewState.empty: return emptyView(); case ViewState.error: @@ -42,9 +46,10 @@ Widget loadingView({ }) { return Center( child: CircularProgressIndicator( + strokeWidth: 3, color: color, valueColor: valueColor, - backgroundColor: backgroundColor ?? Colors.white, + backgroundColor: backgroundColor, value: value, ), ); diff --git a/lib/data/sideb/api/music_api.dart b/lib/data/api/music_api.dart similarity index 78% rename from lib/data/sideb/api/music_api.dart rename to lib/data/api/music_api.dart index 70f6e4b..672fa8c 100644 --- a/lib/data/sideb/api/music_api.dart +++ b/lib/data/api/music_api.dart @@ -3,18 +3,22 @@ // Description: 音乐播放器Api import 'package:devicelocale/devicelocale.dart'; -import 'package:tone_snap/data/app_config.dart'; -import 'package:tone_snap/data/sideb/models/browse_model.dart'; +import 'package:tone_snap/data/models/player_model.dart'; +import 'package:tone_snap/global/app_config.dart'; +import 'package:tone_snap/data/models/browse_model.dart'; import 'package:tone_snap/data/network/dio_client.dart'; -import 'package:tone_snap/data/sideb/models/next_model.dart'; -import 'package:tone_snap/data/sideb/models/player_model.dart'; +import 'package:tone_snap/data/models/next_model.dart'; import 'package:tone_snap/utils/date_util.dart'; class MusicApi { static const String baseUrl = 'https://music.youtube.com/youtubei/v1/'; /// 首页browse接口 - static Future browse({String? visitorData, Map? queryParameters}) async { + static Future browse({ + String? visitorData, + Map? queryParameters, + T Function(Map)? formJson, + }) async { String date = DateUtil.getSevenDaysAgo(); String locale = await Devicelocale.currentLocale ?? AppConfig.defaultLocale; final body = { @@ -26,20 +30,21 @@ class MusicApi { "clientVersion": "1.20240607.01.00", "platform": "DESKTOP", "hl": locale, - "gl": AppConfig.isoCode + // "gl": AppConfig.isoCode + "gl": 'HK' } } }; - BrowseModel? browseModel; - await DioClient().request( + T? resultModel; + await DioClient().request( 'browse', requestMethod: RequestMethod.post, data: body, queryParameters: queryParameters, - formJson: BrowseModel.fromMap, - success: (model) => browseModel = model, + formJson: formJson, + success: (model) => resultModel = model, ); - return browseModel; + return resultModel; } /// next接口 @@ -54,7 +59,8 @@ class MusicApi { "clientVersion": "1.$date", "platform": "DESKTOP", "hl": locale, - "gl": AppConfig.isoCode + // "gl": AppConfig.isoCode + "gl": 'HK' } } }; @@ -66,6 +72,8 @@ class MusicApi { NextModel? nextModel; await DioClient().request( 'next', + showLoading: true, + showToast: true, requestMethod: RequestMethod.post, data: body, queryParameters: queryParameters, @@ -96,6 +104,8 @@ class MusicApi { PlayerModel? playerModel; await DioClient().request( 'player', + showLoading: true, + showToast: true, requestMethod: RequestMethod.post, data: body, queryParameters: queryParameters, diff --git a/lib/data/sideb/api/tikustok_api.dart b/lib/data/api/tikustok_api.dart similarity index 91% rename from lib/data/sideb/api/tikustok_api.dart rename to lib/data/api/tikustok_api.dart index f06f692..134f599 100644 --- a/lib/data/sideb/api/tikustok_api.dart +++ b/lib/data/api/tikustok_api.dart @@ -4,7 +4,7 @@ import 'package:tone_snap/data/models/base_model.dart'; import 'package:tone_snap/data/network/dio_client.dart'; -import 'package:tone_snap/data/sideb/models/isocode_model.dart'; +import 'package:tone_snap/data/models/isocode_model.dart'; class TikUsTokApi { static const String baseUrl = 'https://api.tikustok.com/'; diff --git a/lib/data/cache/music_cache_manager.dart b/lib/data/cache/music_cache_manager.dart index 6eacb5e..692d202 100644 --- a/lib/data/cache/music_cache_manager.dart +++ b/lib/data/cache/music_cache_manager.dart @@ -1,6 +1,6 @@ // Author: fengshengxiong // Date: 2024/6/25 -// Description: 音乐自定义缓存管理器 +// Description: 自定义音乐缓存管理器 import 'package:flutter_cache_manager/flutter_cache_manager.dart'; @@ -13,4 +13,13 @@ class MusicCacheManager { maxNrOfCacheObjects: 20, ), ); + + static String getCacheKey(String videoId) { + return 'music_$videoId'; + } + + /// 检查是否有缓存 + static Future checkCache(String videoId) async { + return await instance.getFileFromCache(getCacheKey(videoId)); + } } \ No newline at end of file diff --git a/lib/data/sideb/enum/browse_type.dart b/lib/data/enum/browse_type.dart similarity index 57% rename from lib/data/sideb/enum/browse_type.dart rename to lib/data/enum/browse_type.dart index de5afa8..b8ac3f1 100644 --- a/lib/data/sideb/enum/browse_type.dart +++ b/lib/data/enum/browse_type.dart @@ -1,23 +1,34 @@ // Author: fengshengxiong // Date: 2024/6/13 -// Description: BrowseType +// Description: 资源类型 enum BrowseType { - musicVideoTypeAtv(name: 'MUSIC_VIDEO_TYPE_ATV', remark: '电台/单曲'), - musicVideoTypeOmv(name: 'MUSIC_VIDEO_TYPE_OMV', remark: '视频'), - musicPageTypeAlbum(name: 'MUSIC_PAGE_TYPE_ALBUM', remark: '专辑'), - // musicPageTypeArtist(name: 'MUSIC_PAGE_TYPE_ARTIST', remark: '艺术家'), - musicPageTypePlaylist(name: 'MUSIC_PAGE_TYPE_PLAYLIST', remark: '歌单/列表'); - // musicPageTypeTrackLyrics(name: 'MUSIC_PAGE_TYPE_TRACK_LYRICS', remark: '歌词'), - // musicPageTypeTrackRelated(name: 'MUSIC_PAGE_TYPE_TRACK_RELATED', remark: '相关内容'); + /// 电台/单曲 + musicVideoTypeAtv(name: 'MUSIC_VIDEO_TYPE_ATV'), + + /// 视频 + musicVideoTypeOmv(name: 'MUSIC_VIDEO_TYPE_OMV'), + + /// 专辑 + musicPageTypeAlbum(name: 'MUSIC_PAGE_TYPE_ALBUM'), + + /// 艺术家 + // musicPageTypeArtist(name: 'MUSIC_PAGE_TYPE_ARTIST'), + + /// 歌单/列表 + musicPageTypePlaylist(name: 'MUSIC_PAGE_TYPE_PLAYLIST'); + + /// 歌词 + // musicPageTypeTrackLyrics(name: 'MUSIC_PAGE_TYPE_TRACK_LYRICS'), + + /// 相关内容 + // musicPageTypeTrackRelated(name: 'MUSIC_PAGE_TYPE_TRACK_RELATED'); const BrowseType({ required this.name, - required this.remark, }); final String name; - final String remark; } extension BrowseTypeExtension on BrowseType { diff --git a/lib/data/enum/play_mode.dart b/lib/data/enum/play_mode.dart new file mode 100644 index 0000000..d8ae89a --- /dev/null +++ b/lib/data/enum/play_mode.dart @@ -0,0 +1,19 @@ +// Author: fengshengxiong +// Date: 2024/6/13 +// Description: 播放模式 + +import 'package:hive/hive.dart'; + +part 'play_mode.g.dart'; + +@HiveType(typeId: 1) +enum PlayMode { + @HiveField(0) + listLoop, + + @HiveField(1) + random, + + @HiveField(2) + singleCycle; +} diff --git a/lib/data/enum/play_mode.g.dart b/lib/data/enum/play_mode.g.dart new file mode 100644 index 0000000..b284044 --- /dev/null +++ b/lib/data/enum/play_mode.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'play_mode.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class PlayModeAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + PlayMode read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return PlayMode.listLoop; + case 1: + return PlayMode.random; + case 2: + return PlayMode.singleCycle; + default: + return PlayMode.listLoop; + } + } + + @override + void write(BinaryWriter writer, PlayMode obj) { + switch (obj) { + case PlayMode.listLoop: + writer.writeByte(0); + break; + case PlayMode.random: + writer.writeByte(1); + break; + case PlayMode.singleCycle: + writer.writeByte(2); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PlayModeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/models/browse_album_model.dart b/lib/data/models/browse_album_model.dart new file mode 100644 index 0000000..24627d0 --- /dev/null +++ b/lib/data/models/browse_album_model.dart @@ -0,0 +1,2693 @@ +// Author: fengshengxiong +// Date: 2024/6/30 +// Description: Browse-专辑模型 + +import 'dart:convert'; + +class BrowseAlbumModel { + ResponseContext? responseContext; + Contents? contents; + String? trackingParams; + Microformat? microformat; + StraplineThumbnailClass? background; + + BrowseAlbumModel({ + this.responseContext, + this.contents, + this.trackingParams, + this.microformat, + this.background, + }); + + factory BrowseAlbumModel.fromJson(String str) => BrowseAlbumModel.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory BrowseAlbumModel.fromMap(Map json) => BrowseAlbumModel( + responseContext: json["responseContext"] == null ? null : ResponseContext.fromMap(json["responseContext"]), + contents: json["contents"] == null ? null : Contents.fromMap(json["contents"]), + trackingParams: json["trackingParams"], + microformat: json["microformat"] == null ? null : Microformat.fromMap(json["microformat"]), + background: json["background"] == null ? null : StraplineThumbnailClass.fromMap(json["background"]), + ); + + Map toMap() => { + "responseContext": responseContext?.toMap(), + "contents": contents?.toMap(), + "trackingParams": trackingParams, + "microformat": microformat?.toMap(), + "background": background?.toMap(), + }; +} + +class StraplineThumbnailClass { + MusicThumbnailRenderer? musicThumbnailRenderer; + + StraplineThumbnailClass({ + this.musicThumbnailRenderer, + }); + + factory StraplineThumbnailClass.fromJson(String str) => StraplineThumbnailClass.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory StraplineThumbnailClass.fromMap(Map json) => StraplineThumbnailClass( + musicThumbnailRenderer: json["musicThumbnailRenderer"] == null ? null : MusicThumbnailRenderer.fromMap(json["musicThumbnailRenderer"]), + ); + + Map toMap() => { + "musicThumbnailRenderer": musicThumbnailRenderer?.toMap(), + }; +} + +class MusicThumbnailRenderer { + MusicThumbnailRendererThumbnail? thumbnail; + String? thumbnailCrop; + String? thumbnailScale; + String? trackingParams; + + MusicThumbnailRenderer({ + this.thumbnail, + this.thumbnailCrop, + this.thumbnailScale, + this.trackingParams, + }); + + factory MusicThumbnailRenderer.fromJson(String str) => MusicThumbnailRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicThumbnailRenderer.fromMap(Map json) => MusicThumbnailRenderer( + thumbnail: json["thumbnail"] == null ? null : MusicThumbnailRendererThumbnail.fromMap(json["thumbnail"]), + thumbnailCrop: json["thumbnailCrop"], + thumbnailScale: json["thumbnailScale"], + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "thumbnail": thumbnail?.toMap(), + "thumbnailCrop": thumbnailCrop, + "thumbnailScale": thumbnailScale, + "trackingParams": trackingParams, + }; +} + +class MusicThumbnailRendererThumbnail { + List? thumbnails; + + MusicThumbnailRendererThumbnail({ + this.thumbnails, + }); + + factory MusicThumbnailRendererThumbnail.fromJson(String str) => MusicThumbnailRendererThumbnail.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicThumbnailRendererThumbnail.fromMap(Map json) => MusicThumbnailRendererThumbnail( + thumbnails: json["thumbnails"] == null ? [] : List.from(json["thumbnails"]!.map((x) => ThumbnailElement.fromMap(x))), + ); + + Map toMap() => { + "thumbnails": thumbnails == null ? [] : List.from(thumbnails!.map((x) => x.toMap())), + }; +} + +class ThumbnailElement { + String? url; + int? width; + int? height; + + ThumbnailElement({ + this.url, + this.width, + this.height, + }); + + factory ThumbnailElement.fromJson(String str) => ThumbnailElement.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ThumbnailElement.fromMap(Map json) => ThumbnailElement( + url: json["url"], + width: json["width"], + height: json["height"], + ); + + Map toMap() => { + "url": url, + "width": width, + "height": height, + }; +} + +class Contents { + TwoColumnBrowseResultsRenderer? twoColumnBrowseResultsRenderer; + + Contents({ + this.twoColumnBrowseResultsRenderer, + }); + + factory Contents.fromJson(String str) => Contents.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Contents.fromMap(Map json) => Contents( + twoColumnBrowseResultsRenderer: json["twoColumnBrowseResultsRenderer"] == null ? null : TwoColumnBrowseResultsRenderer.fromMap(json["twoColumnBrowseResultsRenderer"]), + ); + + Map toMap() => { + "twoColumnBrowseResultsRenderer": twoColumnBrowseResultsRenderer?.toMap(), + }; +} + +class TwoColumnBrowseResultsRenderer { + SecondaryContents? secondaryContents; + List? tabs; + + TwoColumnBrowseResultsRenderer({ + this.secondaryContents, + this.tabs, + }); + + factory TwoColumnBrowseResultsRenderer.fromJson(String str) => TwoColumnBrowseResultsRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory TwoColumnBrowseResultsRenderer.fromMap(Map json) => TwoColumnBrowseResultsRenderer( + secondaryContents: json["secondaryContents"] == null ? null : SecondaryContents.fromMap(json["secondaryContents"]), + tabs: json["tabs"] == null ? [] : List.from(json["tabs"]!.map((x) => Tab.fromMap(x))), + ); + + Map toMap() => { + "secondaryContents": secondaryContents?.toMap(), + "tabs": tabs == null ? [] : List.from(tabs!.map((x) => x.toMap())), + }; +} + +class SecondaryContents { + SecondaryContentsSectionListRenderer? sectionListRenderer; + + SecondaryContents({ + this.sectionListRenderer, + }); + + factory SecondaryContents.fromJson(String str) => SecondaryContents.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SecondaryContents.fromMap(Map json) => SecondaryContents( + sectionListRenderer: json["sectionListRenderer"] == null ? null : SecondaryContentsSectionListRenderer.fromMap(json["sectionListRenderer"]), + ); + + Map toMap() => { + "sectionListRenderer": sectionListRenderer?.toMap(), + }; +} + +class SecondaryContentsSectionListRenderer { + List? contents; + String? trackingParams; + + SecondaryContentsSectionListRenderer({ + this.contents, + this.trackingParams, + }); + + factory SecondaryContentsSectionListRenderer.fromJson(String str) => SecondaryContentsSectionListRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SecondaryContentsSectionListRenderer.fromMap(Map json) => SecondaryContentsSectionListRenderer( + contents: json["contents"] == null ? [] : List.from(json["contents"]!.map((x) => PurpleContent.fromMap(x))), + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "contents": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), + "trackingParams": trackingParams, + }; +} + +class PurpleContent { + MusicShelfRenderer? musicShelfRenderer; + + PurpleContent({ + this.musicShelfRenderer, + }); + + factory PurpleContent.fromJson(String str) => PurpleContent.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PurpleContent.fromMap(Map json) => PurpleContent( + musicShelfRenderer: json["musicShelfRenderer"] == null ? null : MusicShelfRenderer.fromMap(json["musicShelfRenderer"]), + ); + + Map toMap() => { + "musicShelfRenderer": musicShelfRenderer?.toMap(), + }; +} + +class MusicShelfRenderer { + List? contents; + String? trackingParams; + ShelfDivider? shelfDivider; + bool? contentsMultiSelectable; + + MusicShelfRenderer({ + this.contents, + this.trackingParams, + this.shelfDivider, + this.contentsMultiSelectable, + }); + + factory MusicShelfRenderer.fromJson(String str) => MusicShelfRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicShelfRenderer.fromMap(Map json) => MusicShelfRenderer( + contents: json["contents"] == null ? [] : List.from(json["contents"]!.map((x) => MusicShelfRendererContent.fromMap(x))), + trackingParams: json["trackingParams"], + shelfDivider: json["shelfDivider"] == null ? null : ShelfDivider.fromMap(json["shelfDivider"]), + contentsMultiSelectable: json["contentsMultiSelectable"], + ); + + Map toMap() => { + "contents": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), + "trackingParams": trackingParams, + "shelfDivider": shelfDivider?.toMap(), + "contentsMultiSelectable": contentsMultiSelectable, + }; +} + +class MusicShelfRendererContent { + MusicResponsiveListItemRenderer? musicResponsiveListItemRenderer; + + MusicShelfRendererContent({ + this.musicResponsiveListItemRenderer, + }); + + factory MusicShelfRendererContent.fromJson(String str) => MusicShelfRendererContent.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicShelfRendererContent.fromMap(Map json) => MusicShelfRendererContent( + musicResponsiveListItemRenderer: json["musicResponsiveListItemRenderer"] == null ? null : MusicResponsiveListItemRenderer.fromMap(json["musicResponsiveListItemRenderer"]), + ); + + Map toMap() => { + "musicResponsiveListItemRenderer": musicResponsiveListItemRenderer?.toMap(), + }; +} + +class MusicResponsiveListItemRenderer { + String? trackingParams; + Overlay? overlay; + List? flexColumns; + List? fixedColumns; + Menu? menu; + PlaylistItemData? playlistItemData; + String? itemHeight; + Index? index; + MultiSelectCheckbox? multiSelectCheckbox; + + MusicResponsiveListItemRenderer({ + this.trackingParams, + this.overlay, + this.flexColumns, + this.fixedColumns, + this.menu, + this.playlistItemData, + this.itemHeight, + this.index, + this.multiSelectCheckbox, + }); + + factory MusicResponsiveListItemRenderer.fromJson(String str) => MusicResponsiveListItemRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicResponsiveListItemRenderer.fromMap(Map json) => MusicResponsiveListItemRenderer( + trackingParams: json["trackingParams"], + overlay: json["overlay"] == null ? null : Overlay.fromMap(json["overlay"]), + flexColumns: json["flexColumns"] == null ? [] : List.from(json["flexColumns"]!.map((x) => FlexColumn.fromMap(x))), + fixedColumns: json["fixedColumns"] == null ? [] : List.from(json["fixedColumns"]!.map((x) => FixedColumn.fromMap(x))), + menu: json["menu"] == null ? null : Menu.fromMap(json["menu"]), + playlistItemData: json["playlistItemData"] == null ? null : PlaylistItemData.fromMap(json["playlistItemData"]), + itemHeight: json["itemHeight"], + index: json["index"] == null ? null : Index.fromMap(json["index"]), + multiSelectCheckbox: json["multiSelectCheckbox"] == null ? null : MultiSelectCheckbox.fromMap(json["multiSelectCheckbox"]), + ); + + Map toMap() => { + "trackingParams": trackingParams, + "overlay": overlay?.toMap(), + "flexColumns": flexColumns == null ? [] : List.from(flexColumns!.map((x) => x.toMap())), + "fixedColumns": fixedColumns == null ? [] : List.from(fixedColumns!.map((x) => x.toMap())), + "menu": menu?.toMap(), + "playlistItemData": playlistItemData?.toMap(), + "itemHeight": itemHeight, + "index": index?.toMap(), + "multiSelectCheckbox": multiSelectCheckbox?.toMap(), + }; +} + +class FixedColumn { + MusicResponsiveListItemFixedColumnRenderer? musicResponsiveListItemFixedColumnRenderer; + + FixedColumn({ + this.musicResponsiveListItemFixedColumnRenderer, + }); + + factory FixedColumn.fromJson(String str) => FixedColumn.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory FixedColumn.fromMap(Map json) => FixedColumn( + musicResponsiveListItemFixedColumnRenderer: json["musicResponsiveListItemFixedColumnRenderer"] == null ? null : MusicResponsiveListItemFixedColumnRenderer.fromMap(json["musicResponsiveListItemFixedColumnRenderer"]), + ); + + Map toMap() => { + "musicResponsiveListItemFixedColumnRenderer": musicResponsiveListItemFixedColumnRenderer?.toMap(), + }; +} + +class MusicResponsiveListItemFixedColumnRenderer { + Index? text; + String? displayPriority; + String? size; + + MusicResponsiveListItemFixedColumnRenderer({ + this.text, + this.displayPriority, + this.size, + }); + + factory MusicResponsiveListItemFixedColumnRenderer.fromJson(String str) => MusicResponsiveListItemFixedColumnRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicResponsiveListItemFixedColumnRenderer.fromMap(Map json) => MusicResponsiveListItemFixedColumnRenderer( + text: json["text"] == null ? null : Index.fromMap(json["text"]), + displayPriority: json["displayPriority"], + size: json["size"], + ); + + Map toMap() => { + "text": text?.toMap(), + "displayPriority": displayPriority, + "size": size, + }; +} + +class Index { + List? runs; + + Index({ + this.runs, + }); + + factory Index.fromJson(String str) => Index.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Index.fromMap(Map json) => Index( + runs: json["runs"] == null ? [] : List.from(json["runs"]!.map((x) => IndexRun.fromMap(x))), + ); + + Map toMap() => { + "runs": runs == null ? [] : List.from(runs!.map((x) => x.toMap())), + }; +} + +class IndexRun { + String? text; + + IndexRun({ + this.text, + }); + + factory IndexRun.fromJson(String str) => IndexRun.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory IndexRun.fromMap(Map json) => IndexRun( + text: json["text"], + ); + + Map toMap() => { + "text": text, + }; +} + +class FlexColumn { + MusicResponsiveListItemFlexColumnRenderer? musicResponsiveListItemFlexColumnRenderer; + + FlexColumn({ + this.musicResponsiveListItemFlexColumnRenderer, + }); + + factory FlexColumn.fromJson(String str) => FlexColumn.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory FlexColumn.fromMap(Map json) => FlexColumn( + musicResponsiveListItemFlexColumnRenderer: json["musicResponsiveListItemFlexColumnRenderer"] == null ? null : MusicResponsiveListItemFlexColumnRenderer.fromMap(json["musicResponsiveListItemFlexColumnRenderer"]), + ); + + Map toMap() => { + "musicResponsiveListItemFlexColumnRenderer": musicResponsiveListItemFlexColumnRenderer?.toMap(), + }; +} + +class MusicResponsiveListItemFlexColumnRenderer { + Text? text; + String? displayPriority; + + MusicResponsiveListItemFlexColumnRenderer({ + this.text, + this.displayPriority, + }); + + factory MusicResponsiveListItemFlexColumnRenderer.fromJson(String str) => MusicResponsiveListItemFlexColumnRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicResponsiveListItemFlexColumnRenderer.fromMap(Map json) => MusicResponsiveListItemFlexColumnRenderer( + text: json["text"] == null ? null : Text.fromMap(json["text"]), + displayPriority: json["displayPriority"], + ); + + Map toMap() => { + "text": text?.toMap(), + "displayPriority": displayPriority, + }; +} + +class Text { + List? runs; + + Text({ + this.runs, + }); + + factory Text.fromJson(String str) => Text.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Text.fromMap(Map json) => Text( + runs: json["runs"] == null ? [] : List.from(json["runs"]!.map((x) => PurpleRun.fromMap(x))), + ); + + Map toMap() => { + "runs": runs == null ? [] : List.from(runs!.map((x) => x.toMap())), + }; +} + +class PurpleRun { + String? text; + NavigationEndpoint? navigationEndpoint; + + PurpleRun({ + this.text, + this.navigationEndpoint, + }); + + factory PurpleRun.fromJson(String str) => PurpleRun.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PurpleRun.fromMap(Map json) => PurpleRun( + text: json["text"], + navigationEndpoint: json["navigationEndpoint"] == null ? null : NavigationEndpoint.fromMap(json["navigationEndpoint"]), + ); + + Map toMap() => { + "text": text, + "navigationEndpoint": navigationEndpoint?.toMap(), + }; +} + +class NavigationEndpoint { + String? clickTrackingParams; + PlayNavigationEndpointWatchEndpoint? watchEndpoint; + + NavigationEndpoint({ + this.clickTrackingParams, + this.watchEndpoint, + }); + + factory NavigationEndpoint.fromJson(String str) => NavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory NavigationEndpoint.fromMap(Map json) => NavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + watchEndpoint: json["watchEndpoint"] == null ? null : PlayNavigationEndpointWatchEndpoint.fromMap(json["watchEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "watchEndpoint": watchEndpoint?.toMap(), + }; +} + +class PlayNavigationEndpointWatchEndpoint { + String? videoId; + String? playlistId; + LoggingContext? loggingContext; + WatchEndpointMusicSupportedConfigs? watchEndpointMusicSupportedConfigs; + String? params; + int? index; + + PlayNavigationEndpointWatchEndpoint({ + this.videoId, + this.playlistId, + this.loggingContext, + this.watchEndpointMusicSupportedConfigs, + this.params, + this.index, + }); + + factory PlayNavigationEndpointWatchEndpoint.fromJson(String str) => PlayNavigationEndpointWatchEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PlayNavigationEndpointWatchEndpoint.fromMap(Map json) => PlayNavigationEndpointWatchEndpoint( + videoId: json["videoId"], + playlistId: json["playlistId"], + loggingContext: json["loggingContext"] == null ? null : LoggingContext.fromMap(json["loggingContext"]), + watchEndpointMusicSupportedConfigs: json["watchEndpointMusicSupportedConfigs"] == null ? null : WatchEndpointMusicSupportedConfigs.fromMap(json["watchEndpointMusicSupportedConfigs"]), + params: json["params"], + index: json["index"], + ); + + Map toMap() => { + "videoId": videoId, + "playlistId": playlistId, + "loggingContext": loggingContext?.toMap(), + "watchEndpointMusicSupportedConfigs": watchEndpointMusicSupportedConfigs?.toMap(), + "params": params, + "index": index, + }; +} + +class LoggingContext { + VssLoggingContext? vssLoggingContext; + + LoggingContext({ + this.vssLoggingContext, + }); + + factory LoggingContext.fromJson(String str) => LoggingContext.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory LoggingContext.fromMap(Map json) => LoggingContext( + vssLoggingContext: json["vssLoggingContext"] == null ? null : VssLoggingContext.fromMap(json["vssLoggingContext"]), + ); + + Map toMap() => { + "vssLoggingContext": vssLoggingContext?.toMap(), + }; +} + +class VssLoggingContext { + String? serializedContextData; + + VssLoggingContext({ + this.serializedContextData, + }); + + factory VssLoggingContext.fromJson(String str) => VssLoggingContext.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory VssLoggingContext.fromMap(Map json) => VssLoggingContext( + serializedContextData: json["serializedContextData"], + ); + + Map toMap() => { + "serializedContextData": serializedContextData, + }; +} + +class WatchEndpointMusicSupportedConfigs { + WatchEndpointMusicConfig? watchEndpointMusicConfig; + + WatchEndpointMusicSupportedConfigs({ + this.watchEndpointMusicConfig, + }); + + factory WatchEndpointMusicSupportedConfigs.fromJson(String str) => WatchEndpointMusicSupportedConfigs.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory WatchEndpointMusicSupportedConfigs.fromMap(Map json) => WatchEndpointMusicSupportedConfigs( + watchEndpointMusicConfig: json["watchEndpointMusicConfig"] == null ? null : WatchEndpointMusicConfig.fromMap(json["watchEndpointMusicConfig"]), + ); + + Map toMap() => { + "watchEndpointMusicConfig": watchEndpointMusicConfig?.toMap(), + }; +} + +class WatchEndpointMusicConfig { + String? musicVideoType; + + WatchEndpointMusicConfig({ + this.musicVideoType, + }); + + factory WatchEndpointMusicConfig.fromJson(String str) => WatchEndpointMusicConfig.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory WatchEndpointMusicConfig.fromMap(Map json) => WatchEndpointMusicConfig( + musicVideoType: json["musicVideoType"], + ); + + Map toMap() => { + "musicVideoType": musicVideoType, + }; +} + +class Menu { + MenuMenuRenderer? menuRenderer; + + Menu({ + this.menuRenderer, + }); + + factory Menu.fromJson(String str) => Menu.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Menu.fromMap(Map json) => Menu( + menuRenderer: json["menuRenderer"] == null ? null : MenuMenuRenderer.fromMap(json["menuRenderer"]), + ); + + Map toMap() => { + "menuRenderer": menuRenderer?.toMap(), + }; +} + +class MenuMenuRenderer { + List? items; + String? trackingParams; + List? topLevelButtons; + Accessibility? accessibility; + + MenuMenuRenderer({ + this.items, + this.trackingParams, + this.topLevelButtons, + this.accessibility, + }); + + factory MenuMenuRenderer.fromJson(String str) => MenuMenuRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MenuMenuRenderer.fromMap(Map json) => MenuMenuRenderer( + items: json["items"] == null ? [] : List.from(json["items"]!.map((x) => PurpleItem.fromMap(x))), + trackingParams: json["trackingParams"], + topLevelButtons: json["topLevelButtons"] == null ? [] : List.from(json["topLevelButtons"]!.map((x) => TopLevelButton.fromMap(x))), + accessibility: json["accessibility"] == null ? null : Accessibility.fromMap(json["accessibility"]), + ); + + Map toMap() => { + "items": items == null ? [] : List.from(items!.map((x) => x.toMap())), + "trackingParams": trackingParams, + "topLevelButtons": topLevelButtons == null ? [] : List.from(topLevelButtons!.map((x) => x.toMap())), + "accessibility": accessibility?.toMap(), + }; +} + +class Accessibility { + AccessibilityData? accessibilityData; + + Accessibility({ + this.accessibilityData, + }); + + factory Accessibility.fromJson(String str) => Accessibility.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Accessibility.fromMap(Map json) => Accessibility( + accessibilityData: json["accessibilityData"] == null ? null : AccessibilityData.fromMap(json["accessibilityData"]), + ); + + Map toMap() => { + "accessibilityData": accessibilityData?.toMap(), + }; +} + +class AccessibilityData { + String? label; + + AccessibilityData({ + this.label, + }); + + factory AccessibilityData.fromJson(String str) => AccessibilityData.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory AccessibilityData.fromMap(Map json) => AccessibilityData( + label: json["label"], + ); + + Map toMap() => { + "label": label, + }; +} + +class PurpleItem { + MenuItemRenderer? menuNavigationItemRenderer; + MenuItemRenderer? menuServiceItemRenderer; + + PurpleItem({ + this.menuNavigationItemRenderer, + this.menuServiceItemRenderer, + }); + + factory PurpleItem.fromJson(String str) => PurpleItem.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PurpleItem.fromMap(Map json) => PurpleItem( + menuNavigationItemRenderer: json["menuNavigationItemRenderer"] == null ? null : MenuItemRenderer.fromMap(json["menuNavigationItemRenderer"]), + menuServiceItemRenderer: json["menuServiceItemRenderer"] == null ? null : MenuItemRenderer.fromMap(json["menuServiceItemRenderer"]), + ); + + Map toMap() => { + "menuNavigationItemRenderer": menuNavigationItemRenderer?.toMap(), + "menuServiceItemRenderer": menuServiceItemRenderer?.toMap(), + }; +} + +class MenuItemRenderer { + Index? text; + Icon? icon; + MenuNavigationItemRendererNavigationEndpoint? navigationEndpoint; + String? trackingParams; + ServiceEndpoint? serviceEndpoint; + + MenuItemRenderer({ + this.text, + this.icon, + this.navigationEndpoint, + this.trackingParams, + this.serviceEndpoint, + }); + + factory MenuItemRenderer.fromJson(String str) => MenuItemRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MenuItemRenderer.fromMap(Map json) => MenuItemRenderer( + text: json["text"] == null ? null : Index.fromMap(json["text"]), + icon: json["icon"] == null ? null : Icon.fromMap(json["icon"]), + navigationEndpoint: json["navigationEndpoint"] == null ? null : MenuNavigationItemRendererNavigationEndpoint.fromMap(json["navigationEndpoint"]), + trackingParams: json["trackingParams"], + serviceEndpoint: json["serviceEndpoint"] == null ? null : ServiceEndpoint.fromMap(json["serviceEndpoint"]), + ); + + Map toMap() => { + "text": text?.toMap(), + "icon": icon?.toMap(), + "navigationEndpoint": navigationEndpoint?.toMap(), + "trackingParams": trackingParams, + "serviceEndpoint": serviceEndpoint?.toMap(), + }; +} + +class Icon { + String? iconType; + + Icon({ + this.iconType, + }); + + factory Icon.fromJson(String str) => Icon.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Icon.fromMap(Map json) => Icon( + iconType: json["iconType"], + ); + + Map toMap() => { + "iconType": iconType, + }; +} + +class MenuNavigationItemRendererNavigationEndpoint { + String? clickTrackingParams; + PlayNavigationEndpointWatchEndpoint? watchEndpoint; + ModalEndpoint? modalEndpoint; + BrowseEndpoint? browseEndpoint; + ShareEntityEndpoint? shareEntityEndpoint; + WatchPlaylistEndpoint? watchPlaylistEndpoint; + + MenuNavigationItemRendererNavigationEndpoint({ + this.clickTrackingParams, + this.watchEndpoint, + this.modalEndpoint, + this.browseEndpoint, + this.shareEntityEndpoint, + this.watchPlaylistEndpoint, + }); + + factory MenuNavigationItemRendererNavigationEndpoint.fromJson(String str) => MenuNavigationItemRendererNavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MenuNavigationItemRendererNavigationEndpoint.fromMap(Map json) => MenuNavigationItemRendererNavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + watchEndpoint: json["watchEndpoint"] == null ? null : PlayNavigationEndpointWatchEndpoint.fromMap(json["watchEndpoint"]), + modalEndpoint: json["modalEndpoint"] == null ? null : ModalEndpoint.fromMap(json["modalEndpoint"]), + browseEndpoint: json["browseEndpoint"] == null ? null : BrowseEndpoint.fromMap(json["browseEndpoint"]), + shareEntityEndpoint: json["shareEntityEndpoint"] == null ? null : ShareEntityEndpoint.fromMap(json["shareEntityEndpoint"]), + watchPlaylistEndpoint: json["watchPlaylistEndpoint"] == null ? null : WatchPlaylistEndpoint.fromMap(json["watchPlaylistEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "watchEndpoint": watchEndpoint?.toMap(), + "modalEndpoint": modalEndpoint?.toMap(), + "browseEndpoint": browseEndpoint?.toMap(), + "shareEntityEndpoint": shareEntityEndpoint?.toMap(), + "watchPlaylistEndpoint": watchPlaylistEndpoint?.toMap(), + }; +} + +class BrowseEndpoint { + String? browseId; + BrowseEndpointContextSupportedConfigs? browseEndpointContextSupportedConfigs; + + BrowseEndpoint({ + this.browseId, + this.browseEndpointContextSupportedConfigs, + }); + + factory BrowseEndpoint.fromJson(String str) => BrowseEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory BrowseEndpoint.fromMap(Map json) => BrowseEndpoint( + browseId: json["browseId"], + browseEndpointContextSupportedConfigs: json["browseEndpointContextSupportedConfigs"] == null ? null : BrowseEndpointContextSupportedConfigs.fromMap(json["browseEndpointContextSupportedConfigs"]), + ); + + Map toMap() => { + "browseId": browseId, + "browseEndpointContextSupportedConfigs": browseEndpointContextSupportedConfigs?.toMap(), + }; +} + +class BrowseEndpointContextSupportedConfigs { + BrowseEndpointContextMusicConfig? browseEndpointContextMusicConfig; + + BrowseEndpointContextSupportedConfigs({ + this.browseEndpointContextMusicConfig, + }); + + factory BrowseEndpointContextSupportedConfigs.fromJson(String str) => BrowseEndpointContextSupportedConfigs.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory BrowseEndpointContextSupportedConfigs.fromMap(Map json) => BrowseEndpointContextSupportedConfigs( + browseEndpointContextMusicConfig: json["browseEndpointContextMusicConfig"] == null ? null : BrowseEndpointContextMusicConfig.fromMap(json["browseEndpointContextMusicConfig"]), + ); + + Map toMap() => { + "browseEndpointContextMusicConfig": browseEndpointContextMusicConfig?.toMap(), + }; +} + +class BrowseEndpointContextMusicConfig { + String? pageType; + + BrowseEndpointContextMusicConfig({ + this.pageType, + }); + + factory BrowseEndpointContextMusicConfig.fromJson(String str) => BrowseEndpointContextMusicConfig.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory BrowseEndpointContextMusicConfig.fromMap(Map json) => BrowseEndpointContextMusicConfig( + pageType: json["pageType"], + ); + + Map toMap() => { + "pageType": pageType, + }; +} + +class ModalEndpoint { + Modal? modal; + + ModalEndpoint({ + this.modal, + }); + + factory ModalEndpoint.fromJson(String str) => ModalEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ModalEndpoint.fromMap(Map json) => ModalEndpoint( + modal: json["modal"] == null ? null : Modal.fromMap(json["modal"]), + ); + + Map toMap() => { + "modal": modal?.toMap(), + }; +} + +class Modal { + ModalWithTitleAndButtonRenderer? modalWithTitleAndButtonRenderer; + + Modal({ + this.modalWithTitleAndButtonRenderer, + }); + + factory Modal.fromJson(String str) => Modal.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Modal.fromMap(Map json) => Modal( + modalWithTitleAndButtonRenderer: json["modalWithTitleAndButtonRenderer"] == null ? null : ModalWithTitleAndButtonRenderer.fromMap(json["modalWithTitleAndButtonRenderer"]), + ); + + Map toMap() => { + "modalWithTitleAndButtonRenderer": modalWithTitleAndButtonRenderer?.toMap(), + }; +} + +class ModalWithTitleAndButtonRenderer { + Index? title; + Index? content; + ModalWithTitleAndButtonRendererButton? button; + + ModalWithTitleAndButtonRenderer({ + this.title, + this.content, + this.button, + }); + + factory ModalWithTitleAndButtonRenderer.fromJson(String str) => ModalWithTitleAndButtonRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ModalWithTitleAndButtonRenderer.fromMap(Map json) => ModalWithTitleAndButtonRenderer( + title: json["title"] == null ? null : Index.fromMap(json["title"]), + content: json["content"] == null ? null : Index.fromMap(json["content"]), + button: json["button"] == null ? null : ModalWithTitleAndButtonRendererButton.fromMap(json["button"]), + ); + + Map toMap() => { + "title": title?.toMap(), + "content": content?.toMap(), + "button": button?.toMap(), + }; +} + +class ModalWithTitleAndButtonRendererButton { + ButtonRenderer? buttonRenderer; + + ModalWithTitleAndButtonRendererButton({ + this.buttonRenderer, + }); + + factory ModalWithTitleAndButtonRendererButton.fromJson(String str) => ModalWithTitleAndButtonRendererButton.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ModalWithTitleAndButtonRendererButton.fromMap(Map json) => ModalWithTitleAndButtonRendererButton( + buttonRenderer: json["buttonRenderer"] == null ? null : ButtonRenderer.fromMap(json["buttonRenderer"]), + ); + + Map toMap() => { + "buttonRenderer": buttonRenderer?.toMap(), + }; +} + +class ButtonRenderer { + String? style; + bool? isDisabled; + Index? text; + ButtonRendererNavigationEndpoint? navigationEndpoint; + String? trackingParams; + + ButtonRenderer({ + this.style, + this.isDisabled, + this.text, + this.navigationEndpoint, + this.trackingParams, + }); + + factory ButtonRenderer.fromJson(String str) => ButtonRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ButtonRenderer.fromMap(Map json) => ButtonRenderer( + style: json["style"], + isDisabled: json["isDisabled"], + text: json["text"] == null ? null : Index.fromMap(json["text"]), + navigationEndpoint: json["navigationEndpoint"] == null ? null : ButtonRendererNavigationEndpoint.fromMap(json["navigationEndpoint"]), + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "style": style, + "isDisabled": isDisabled, + "text": text?.toMap(), + "navigationEndpoint": navigationEndpoint?.toMap(), + "trackingParams": trackingParams, + }; +} + +class ButtonRendererNavigationEndpoint { + String? clickTrackingParams; + SignInEndpoint? signInEndpoint; + + ButtonRendererNavigationEndpoint({ + this.clickTrackingParams, + this.signInEndpoint, + }); + + factory ButtonRendererNavigationEndpoint.fromJson(String str) => ButtonRendererNavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ButtonRendererNavigationEndpoint.fromMap(Map json) => ButtonRendererNavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + signInEndpoint: json["signInEndpoint"] == null ? null : SignInEndpoint.fromMap(json["signInEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "signInEndpoint": signInEndpoint?.toMap(), + }; +} + +class SignInEndpoint { + bool? hack; + + SignInEndpoint({ + this.hack, + }); + + factory SignInEndpoint.fromJson(String str) => SignInEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SignInEndpoint.fromMap(Map json) => SignInEndpoint( + hack: json["hack"], + ); + + Map toMap() => { + "hack": hack, + }; +} + +class ShareEntityEndpoint { + String? serializedShareEntity; + String? sharePanelType; + + ShareEntityEndpoint({ + this.serializedShareEntity, + this.sharePanelType, + }); + + factory ShareEntityEndpoint.fromJson(String str) => ShareEntityEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ShareEntityEndpoint.fromMap(Map json) => ShareEntityEndpoint( + serializedShareEntity: json["serializedShareEntity"], + sharePanelType: json["sharePanelType"], + ); + + Map toMap() => { + "serializedShareEntity": serializedShareEntity, + "sharePanelType": sharePanelType, + }; +} + +class WatchPlaylistEndpoint { + String? playlistId; + String? params; + + WatchPlaylistEndpoint({ + this.playlistId, + this.params, + }); + + factory WatchPlaylistEndpoint.fromJson(String str) => WatchPlaylistEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory WatchPlaylistEndpoint.fromMap(Map json) => WatchPlaylistEndpoint( + playlistId: json["playlistId"], + params: json["params"], + ); + + Map toMap() => { + "playlistId": playlistId, + "params": params, + }; +} + +class ServiceEndpoint { + String? clickTrackingParams; + QueueAddEndpoint? queueAddEndpoint; + + ServiceEndpoint({ + this.clickTrackingParams, + this.queueAddEndpoint, + }); + + factory ServiceEndpoint.fromJson(String str) => ServiceEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ServiceEndpoint.fromMap(Map json) => ServiceEndpoint( + clickTrackingParams: json["clickTrackingParams"], + queueAddEndpoint: json["queueAddEndpoint"] == null ? null : QueueAddEndpoint.fromMap(json["queueAddEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "queueAddEndpoint": queueAddEndpoint?.toMap(), + }; +} + +class QueueAddEndpoint { + QueueTarget? queueTarget; + String? queueInsertPosition; + List? commands; + + QueueAddEndpoint({ + this.queueTarget, + this.queueInsertPosition, + this.commands, + }); + + factory QueueAddEndpoint.fromJson(String str) => QueueAddEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory QueueAddEndpoint.fromMap(Map json) => QueueAddEndpoint( + queueTarget: json["queueTarget"] == null ? null : QueueTarget.fromMap(json["queueTarget"]), + queueInsertPosition: json["queueInsertPosition"], + commands: json["commands"] == null ? [] : List.from(json["commands"]!.map((x) => Command.fromMap(x))), + ); + + Map toMap() => { + "queueTarget": queueTarget?.toMap(), + "queueInsertPosition": queueInsertPosition, + "commands": commands == null ? [] : List.from(commands!.map((x) => x.toMap())), + }; +} + +class Command { + String? clickTrackingParams; + AddToToastAction? addToToastAction; + + Command({ + this.clickTrackingParams, + this.addToToastAction, + }); + + factory Command.fromJson(String str) => Command.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Command.fromMap(Map json) => Command( + clickTrackingParams: json["clickTrackingParams"], + addToToastAction: json["addToToastAction"] == null ? null : AddToToastAction.fromMap(json["addToToastAction"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "addToToastAction": addToToastAction?.toMap(), + }; +} + +class AddToToastAction { + AddToToastActionItem? item; + + AddToToastAction({ + this.item, + }); + + factory AddToToastAction.fromJson(String str) => AddToToastAction.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory AddToToastAction.fromMap(Map json) => AddToToastAction( + item: json["item"] == null ? null : AddToToastActionItem.fromMap(json["item"]), + ); + + Map toMap() => { + "item": item?.toMap(), + }; +} + +class AddToToastActionItem { + NotificationTextRenderer? notificationTextRenderer; + + AddToToastActionItem({ + this.notificationTextRenderer, + }); + + factory AddToToastActionItem.fromJson(String str) => AddToToastActionItem.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory AddToToastActionItem.fromMap(Map json) => AddToToastActionItem( + notificationTextRenderer: json["notificationTextRenderer"] == null ? null : NotificationTextRenderer.fromMap(json["notificationTextRenderer"]), + ); + + Map toMap() => { + "notificationTextRenderer": notificationTextRenderer?.toMap(), + }; +} + +class NotificationTextRenderer { + Index? successResponseText; + String? trackingParams; + + NotificationTextRenderer({ + this.successResponseText, + this.trackingParams, + }); + + factory NotificationTextRenderer.fromJson(String str) => NotificationTextRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory NotificationTextRenderer.fromMap(Map json) => NotificationTextRenderer( + successResponseText: json["successResponseText"] == null ? null : Index.fromMap(json["successResponseText"]), + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "successResponseText": successResponseText?.toMap(), + "trackingParams": trackingParams, + }; +} + +class QueueTarget { + String? videoId; + OnEmptyQueue? onEmptyQueue; + String? playlistId; + + QueueTarget({ + this.videoId, + this.onEmptyQueue, + this.playlistId, + }); + + factory QueueTarget.fromJson(String str) => QueueTarget.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory QueueTarget.fromMap(Map json) => QueueTarget( + videoId: json["videoId"], + onEmptyQueue: json["onEmptyQueue"] == null ? null : OnEmptyQueue.fromMap(json["onEmptyQueue"]), + playlistId: json["playlistId"], + ); + + Map toMap() => { + "videoId": videoId, + "onEmptyQueue": onEmptyQueue?.toMap(), + "playlistId": playlistId, + }; +} + +class OnEmptyQueue { + String? clickTrackingParams; + OnEmptyQueueWatchEndpoint? watchEndpoint; + + OnEmptyQueue({ + this.clickTrackingParams, + this.watchEndpoint, + }); + + factory OnEmptyQueue.fromJson(String str) => OnEmptyQueue.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory OnEmptyQueue.fromMap(Map json) => OnEmptyQueue( + clickTrackingParams: json["clickTrackingParams"], + watchEndpoint: json["watchEndpoint"] == null ? null : OnEmptyQueueWatchEndpoint.fromMap(json["watchEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "watchEndpoint": watchEndpoint?.toMap(), + }; +} + +class OnEmptyQueueWatchEndpoint { + String? videoId; + String? playlistId; + + OnEmptyQueueWatchEndpoint({ + this.videoId, + this.playlistId, + }); + + factory OnEmptyQueueWatchEndpoint.fromJson(String str) => OnEmptyQueueWatchEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory OnEmptyQueueWatchEndpoint.fromMap(Map json) => OnEmptyQueueWatchEndpoint( + videoId: json["videoId"], + playlistId: json["playlistId"], + ); + + Map toMap() => { + "videoId": videoId, + "playlistId": playlistId, + }; +} + +class TopLevelButton { + LikeButtonRenderer? likeButtonRenderer; + + TopLevelButton({ + this.likeButtonRenderer, + }); + + factory TopLevelButton.fromJson(String str) => TopLevelButton.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory TopLevelButton.fromMap(Map json) => TopLevelButton( + likeButtonRenderer: json["likeButtonRenderer"] == null ? null : LikeButtonRenderer.fromMap(json["likeButtonRenderer"]), + ); + + Map toMap() => { + "likeButtonRenderer": likeButtonRenderer?.toMap(), + }; +} + +class LikeButtonRenderer { + LikeButtonRendererTarget? target; + String? likeStatus; + String? trackingParams; + bool? likesAllowed; + DefaultNavigationEndpoint? dislikeNavigationEndpoint; + DefaultNavigationEndpoint? likeCommand; + + LikeButtonRenderer({ + this.target, + this.likeStatus, + this.trackingParams, + this.likesAllowed, + this.dislikeNavigationEndpoint, + this.likeCommand, + }); + + factory LikeButtonRenderer.fromJson(String str) => LikeButtonRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory LikeButtonRenderer.fromMap(Map json) => LikeButtonRenderer( + target: json["target"] == null ? null : LikeButtonRendererTarget.fromMap(json["target"]), + likeStatus: json["likeStatus"], + trackingParams: json["trackingParams"], + likesAllowed: json["likesAllowed"], + dislikeNavigationEndpoint: json["dislikeNavigationEndpoint"] == null ? null : DefaultNavigationEndpoint.fromMap(json["dislikeNavigationEndpoint"]), + likeCommand: json["likeCommand"] == null ? null : DefaultNavigationEndpoint.fromMap(json["likeCommand"]), + ); + + Map toMap() => { + "target": target?.toMap(), + "likeStatus": likeStatus, + "trackingParams": trackingParams, + "likesAllowed": likesAllowed, + "dislikeNavigationEndpoint": dislikeNavigationEndpoint?.toMap(), + "likeCommand": likeCommand?.toMap(), + }; +} + +class DefaultNavigationEndpoint { + String? clickTrackingParams; + ModalEndpoint? modalEndpoint; + + DefaultNavigationEndpoint({ + this.clickTrackingParams, + this.modalEndpoint, + }); + + factory DefaultNavigationEndpoint.fromJson(String str) => DefaultNavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory DefaultNavigationEndpoint.fromMap(Map json) => DefaultNavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + modalEndpoint: json["modalEndpoint"] == null ? null : ModalEndpoint.fromMap(json["modalEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "modalEndpoint": modalEndpoint?.toMap(), + }; +} + +class LikeButtonRendererTarget { + String? videoId; + + LikeButtonRendererTarget({ + this.videoId, + }); + + factory LikeButtonRendererTarget.fromJson(String str) => LikeButtonRendererTarget.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory LikeButtonRendererTarget.fromMap(Map json) => LikeButtonRendererTarget( + videoId: json["videoId"], + ); + + Map toMap() => { + "videoId": videoId, + }; +} + +class MultiSelectCheckbox { + CheckboxRenderer? checkboxRenderer; + + MultiSelectCheckbox({ + this.checkboxRenderer, + }); + + factory MultiSelectCheckbox.fromJson(String str) => MultiSelectCheckbox.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MultiSelectCheckbox.fromMap(Map json) => MultiSelectCheckbox( + checkboxRenderer: json["checkboxRenderer"] == null ? null : CheckboxRenderer.fromMap(json["checkboxRenderer"]), + ); + + Map toMap() => { + "checkboxRenderer": checkboxRenderer?.toMap(), + }; +} + +class CheckboxRenderer { + OnSelectionChangeCommand? onSelectionChangeCommand; + String? checkedState; + String? trackingParams; + + CheckboxRenderer({ + this.onSelectionChangeCommand, + this.checkedState, + this.trackingParams, + }); + + factory CheckboxRenderer.fromJson(String str) => CheckboxRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory CheckboxRenderer.fromMap(Map json) => CheckboxRenderer( + onSelectionChangeCommand: json["onSelectionChangeCommand"] == null ? null : OnSelectionChangeCommand.fromMap(json["onSelectionChangeCommand"]), + checkedState: json["checkedState"], + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "onSelectionChangeCommand": onSelectionChangeCommand?.toMap(), + "checkedState": checkedState, + "trackingParams": trackingParams, + }; +} + +class OnSelectionChangeCommand { + String? clickTrackingParams; + UpdateMultiSelectStateCommand? updateMultiSelectStateCommand; + + OnSelectionChangeCommand({ + this.clickTrackingParams, + this.updateMultiSelectStateCommand, + }); + + factory OnSelectionChangeCommand.fromJson(String str) => OnSelectionChangeCommand.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory OnSelectionChangeCommand.fromMap(Map json) => OnSelectionChangeCommand( + clickTrackingParams: json["clickTrackingParams"], + updateMultiSelectStateCommand: json["updateMultiSelectStateCommand"] == null ? null : UpdateMultiSelectStateCommand.fromMap(json["updateMultiSelectStateCommand"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "updateMultiSelectStateCommand": updateMultiSelectStateCommand?.toMap(), + }; +} + +class UpdateMultiSelectStateCommand { + String? multiSelectParams; + String? multiSelectItem; + + UpdateMultiSelectStateCommand({ + this.multiSelectParams, + this.multiSelectItem, + }); + + factory UpdateMultiSelectStateCommand.fromJson(String str) => UpdateMultiSelectStateCommand.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory UpdateMultiSelectStateCommand.fromMap(Map json) => UpdateMultiSelectStateCommand( + multiSelectParams: json["multiSelectParams"], + multiSelectItem: json["multiSelectItem"], + ); + + Map toMap() => { + "multiSelectParams": multiSelectParams, + "multiSelectItem": multiSelectItem, + }; +} + +class Overlay { + MusicItemThumbnailOverlayRenderer? musicItemThumbnailOverlayRenderer; + + Overlay({ + this.musicItemThumbnailOverlayRenderer, + }); + + factory Overlay.fromJson(String str) => Overlay.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Overlay.fromMap(Map json) => Overlay( + musicItemThumbnailOverlayRenderer: json["musicItemThumbnailOverlayRenderer"] == null ? null : MusicItemThumbnailOverlayRenderer.fromMap(json["musicItemThumbnailOverlayRenderer"]), + ); + + Map toMap() => { + "musicItemThumbnailOverlayRenderer": musicItemThumbnailOverlayRenderer?.toMap(), + }; +} + +class MusicItemThumbnailOverlayRenderer { + MusicItemThumbnailOverlayRendererBackground? background; + MusicItemThumbnailOverlayRendererContent? content; + String? contentPosition; + String? displayStyle; + + MusicItemThumbnailOverlayRenderer({ + this.background, + this.content, + this.contentPosition, + this.displayStyle, + }); + + factory MusicItemThumbnailOverlayRenderer.fromJson(String str) => MusicItemThumbnailOverlayRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicItemThumbnailOverlayRenderer.fromMap(Map json) => MusicItemThumbnailOverlayRenderer( + background: json["background"] == null ? null : MusicItemThumbnailOverlayRendererBackground.fromMap(json["background"]), + content: json["content"] == null ? null : MusicItemThumbnailOverlayRendererContent.fromMap(json["content"]), + contentPosition: json["contentPosition"], + displayStyle: json["displayStyle"], + ); + + Map toMap() => { + "background": background?.toMap(), + "content": content?.toMap(), + "contentPosition": contentPosition, + "displayStyle": displayStyle, + }; +} + +class MusicItemThumbnailOverlayRendererBackground { + VerticalGradient? verticalGradient; + + MusicItemThumbnailOverlayRendererBackground({ + this.verticalGradient, + }); + + factory MusicItemThumbnailOverlayRendererBackground.fromJson(String str) => MusicItemThumbnailOverlayRendererBackground.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicItemThumbnailOverlayRendererBackground.fromMap(Map json) => MusicItemThumbnailOverlayRendererBackground( + verticalGradient: json["verticalGradient"] == null ? null : VerticalGradient.fromMap(json["verticalGradient"]), + ); + + Map toMap() => { + "verticalGradient": verticalGradient?.toMap(), + }; +} + +class VerticalGradient { + List? gradientLayerColors; + + VerticalGradient({ + this.gradientLayerColors, + }); + + factory VerticalGradient.fromJson(String str) => VerticalGradient.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory VerticalGradient.fromMap(Map json) => VerticalGradient( + gradientLayerColors: json["gradientLayerColors"] == null ? [] : List.from(json["gradientLayerColors"]!.map((x) => x)), + ); + + Map toMap() => { + "gradientLayerColors": gradientLayerColors == null ? [] : List.from(gradientLayerColors!.map((x) => x)), + }; +} + +class MusicItemThumbnailOverlayRendererContent { + ContentMusicPlayButtonRenderer? musicPlayButtonRenderer; + + MusicItemThumbnailOverlayRendererContent({ + this.musicPlayButtonRenderer, + }); + + factory MusicItemThumbnailOverlayRendererContent.fromJson(String str) => MusicItemThumbnailOverlayRendererContent.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicItemThumbnailOverlayRendererContent.fromMap(Map json) => MusicItemThumbnailOverlayRendererContent( + musicPlayButtonRenderer: json["musicPlayButtonRenderer"] == null ? null : ContentMusicPlayButtonRenderer.fromMap(json["musicPlayButtonRenderer"]), + ); + + Map toMap() => { + "musicPlayButtonRenderer": musicPlayButtonRenderer?.toMap(), + }; +} + +class ContentMusicPlayButtonRenderer { + NavigationEndpoint? playNavigationEndpoint; + String? trackingParams; + Icon? playIcon; + Icon? pauseIcon; + int? iconColor; + int? backgroundColor; + int? activeBackgroundColor; + int? loadingIndicatorColor; + Icon? playingIcon; + int? iconLoadingColor; + int? activeScaleFactor; + String? buttonSize; + String? rippleTarget; + Accessibility? accessibilityPlayData; + Accessibility? accessibilityPauseData; + + ContentMusicPlayButtonRenderer({ + this.playNavigationEndpoint, + this.trackingParams, + this.playIcon, + this.pauseIcon, + this.iconColor, + this.backgroundColor, + this.activeBackgroundColor, + this.loadingIndicatorColor, + this.playingIcon, + this.iconLoadingColor, + this.activeScaleFactor, + this.buttonSize, + this.rippleTarget, + this.accessibilityPlayData, + this.accessibilityPauseData, + }); + + factory ContentMusicPlayButtonRenderer.fromJson(String str) => ContentMusicPlayButtonRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ContentMusicPlayButtonRenderer.fromMap(Map json) => ContentMusicPlayButtonRenderer( + playNavigationEndpoint: json["playNavigationEndpoint"] == null ? null : NavigationEndpoint.fromMap(json["playNavigationEndpoint"]), + trackingParams: json["trackingParams"], + playIcon: json["playIcon"] == null ? null : Icon.fromMap(json["playIcon"]), + pauseIcon: json["pauseIcon"] == null ? null : Icon.fromMap(json["pauseIcon"]), + iconColor: json["iconColor"], + backgroundColor: json["backgroundColor"], + activeBackgroundColor: json["activeBackgroundColor"], + loadingIndicatorColor: json["loadingIndicatorColor"], + playingIcon: json["playingIcon"] == null ? null : Icon.fromMap(json["playingIcon"]), + iconLoadingColor: json["iconLoadingColor"], + activeScaleFactor: json["activeScaleFactor"], + buttonSize: json["buttonSize"], + rippleTarget: json["rippleTarget"], + accessibilityPlayData: json["accessibilityPlayData"] == null ? null : Accessibility.fromMap(json["accessibilityPlayData"]), + accessibilityPauseData: json["accessibilityPauseData"] == null ? null : Accessibility.fromMap(json["accessibilityPauseData"]), + ); + + Map toMap() => { + "playNavigationEndpoint": playNavigationEndpoint?.toMap(), + "trackingParams": trackingParams, + "playIcon": playIcon?.toMap(), + "pauseIcon": pauseIcon?.toMap(), + "iconColor": iconColor, + "backgroundColor": backgroundColor, + "activeBackgroundColor": activeBackgroundColor, + "loadingIndicatorColor": loadingIndicatorColor, + "playingIcon": playingIcon?.toMap(), + "iconLoadingColor": iconLoadingColor, + "activeScaleFactor": activeScaleFactor, + "buttonSize": buttonSize, + "rippleTarget": rippleTarget, + "accessibilityPlayData": accessibilityPlayData?.toMap(), + "accessibilityPauseData": accessibilityPauseData?.toMap(), + }; +} + +class PlaylistItemData { + String? playlistSetVideoId; + String? videoId; + + PlaylistItemData({ + this.playlistSetVideoId, + this.videoId, + }); + + factory PlaylistItemData.fromJson(String str) => PlaylistItemData.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PlaylistItemData.fromMap(Map json) => PlaylistItemData( + playlistSetVideoId: json["playlistSetVideoId"], + videoId: json["videoId"], + ); + + Map toMap() => { + "playlistSetVideoId": playlistSetVideoId, + "videoId": videoId, + }; +} + +class ShelfDivider { + MusicShelfDividerRenderer? musicShelfDividerRenderer; + + ShelfDivider({ + this.musicShelfDividerRenderer, + }); + + factory ShelfDivider.fromJson(String str) => ShelfDivider.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ShelfDivider.fromMap(Map json) => ShelfDivider( + musicShelfDividerRenderer: json["musicShelfDividerRenderer"] == null ? null : MusicShelfDividerRenderer.fromMap(json["musicShelfDividerRenderer"]), + ); + + Map toMap() => { + "musicShelfDividerRenderer": musicShelfDividerRenderer?.toMap(), + }; +} + +class MusicShelfDividerRenderer { + bool? hidden; + + MusicShelfDividerRenderer({ + this.hidden, + }); + + factory MusicShelfDividerRenderer.fromJson(String str) => MusicShelfDividerRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicShelfDividerRenderer.fromMap(Map json) => MusicShelfDividerRenderer( + hidden: json["hidden"], + ); + + Map toMap() => { + "hidden": hidden, + }; +} + +class Tab { + TabRenderer? tabRenderer; + + Tab({ + this.tabRenderer, + }); + + factory Tab.fromJson(String str) => Tab.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Tab.fromMap(Map json) => Tab( + tabRenderer: json["tabRenderer"] == null ? null : TabRenderer.fromMap(json["tabRenderer"]), + ); + + Map toMap() => { + "tabRenderer": tabRenderer?.toMap(), + }; +} + +class TabRenderer { + TabRendererContent? content; + String? trackingParams; + + TabRenderer({ + this.content, + this.trackingParams, + }); + + factory TabRenderer.fromJson(String str) => TabRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory TabRenderer.fromMap(Map json) => TabRenderer( + content: json["content"] == null ? null : TabRendererContent.fromMap(json["content"]), + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "content": content?.toMap(), + "trackingParams": trackingParams, + }; +} + +class TabRendererContent { + ContentSectionListRenderer? sectionListRenderer; + + TabRendererContent({ + this.sectionListRenderer, + }); + + factory TabRendererContent.fromJson(String str) => TabRendererContent.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory TabRendererContent.fromMap(Map json) => TabRendererContent( + sectionListRenderer: json["sectionListRenderer"] == null ? null : ContentSectionListRenderer.fromMap(json["sectionListRenderer"]), + ); + + Map toMap() => { + "sectionListRenderer": sectionListRenderer?.toMap(), + }; +} + +class ContentSectionListRenderer { + List? contents; + String? trackingParams; + + ContentSectionListRenderer({ + this.contents, + this.trackingParams, + }); + + factory ContentSectionListRenderer.fromJson(String str) => ContentSectionListRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ContentSectionListRenderer.fromMap(Map json) => ContentSectionListRenderer( + contents: json["contents"] == null ? [] : List.from(json["contents"]!.map((x) => FluffyContent.fromMap(x))), + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "contents": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), + "trackingParams": trackingParams, + }; +} + +class FluffyContent { + MusicResponsiveHeaderRenderer? musicResponsiveHeaderRenderer; + + FluffyContent({ + this.musicResponsiveHeaderRenderer, + }); + + factory FluffyContent.fromJson(String str) => FluffyContent.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory FluffyContent.fromMap(Map json) => FluffyContent( + musicResponsiveHeaderRenderer: json["musicResponsiveHeaderRenderer"] == null ? null : MusicResponsiveHeaderRenderer.fromMap(json["musicResponsiveHeaderRenderer"]), + ); + + Map toMap() => { + "musicResponsiveHeaderRenderer": musicResponsiveHeaderRenderer?.toMap(), + }; +} + +class MusicResponsiveHeaderRenderer { + StraplineThumbnailClass? thumbnail; + List? buttons; + Index? title; + Index? subtitle; + String? trackingParams; + StraplineTextOne? straplineTextOne; + StraplineThumbnailClass? straplineThumbnail; + MusicResponsiveHeaderRendererDescription? description; + Index? secondSubtitle; + + MusicResponsiveHeaderRenderer({ + this.thumbnail, + this.buttons, + this.title, + this.subtitle, + this.trackingParams, + this.straplineTextOne, + this.straplineThumbnail, + this.description, + this.secondSubtitle, + }); + + factory MusicResponsiveHeaderRenderer.fromJson(String str) => MusicResponsiveHeaderRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicResponsiveHeaderRenderer.fromMap(Map json) => MusicResponsiveHeaderRenderer( + thumbnail: json["thumbnail"] == null ? null : StraplineThumbnailClass.fromMap(json["thumbnail"]), + buttons: json["buttons"] == null ? [] : List.from(json["buttons"]!.map((x) => ButtonElement.fromMap(x))), + title: json["title"] == null ? null : Index.fromMap(json["title"]), + subtitle: json["subtitle"] == null ? null : Index.fromMap(json["subtitle"]), + trackingParams: json["trackingParams"], + straplineTextOne: json["straplineTextOne"] == null ? null : StraplineTextOne.fromMap(json["straplineTextOne"]), + straplineThumbnail: json["straplineThumbnail"] == null ? null : StraplineThumbnailClass.fromMap(json["straplineThumbnail"]), + description: json["description"] == null ? null : MusicResponsiveHeaderRendererDescription.fromMap(json["description"]), + secondSubtitle: json["secondSubtitle"] == null ? null : Index.fromMap(json["secondSubtitle"]), + ); + + Map toMap() => { + "thumbnail": thumbnail?.toMap(), + "buttons": buttons == null ? [] : List.from(buttons!.map((x) => x.toMap())), + "title": title?.toMap(), + "subtitle": subtitle?.toMap(), + "trackingParams": trackingParams, + "straplineTextOne": straplineTextOne?.toMap(), + "straplineThumbnail": straplineThumbnail?.toMap(), + "description": description?.toMap(), + "secondSubtitle": secondSubtitle?.toMap(), + }; +} + +class ButtonElement { + ButtonToggleButtonRenderer? toggleButtonRenderer; + ButtonMusicPlayButtonRenderer? musicPlayButtonRenderer; + ButtonMenuRenderer? menuRenderer; + + ButtonElement({ + this.toggleButtonRenderer, + this.musicPlayButtonRenderer, + this.menuRenderer, + }); + + factory ButtonElement.fromJson(String str) => ButtonElement.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ButtonElement.fromMap(Map json) => ButtonElement( + toggleButtonRenderer: json["toggleButtonRenderer"] == null ? null : ButtonToggleButtonRenderer.fromMap(json["toggleButtonRenderer"]), + musicPlayButtonRenderer: json["musicPlayButtonRenderer"] == null ? null : ButtonMusicPlayButtonRenderer.fromMap(json["musicPlayButtonRenderer"]), + menuRenderer: json["menuRenderer"] == null ? null : ButtonMenuRenderer.fromMap(json["menuRenderer"]), + ); + + Map toMap() => { + "toggleButtonRenderer": toggleButtonRenderer?.toMap(), + "musicPlayButtonRenderer": musicPlayButtonRenderer?.toMap(), + "menuRenderer": menuRenderer?.toMap(), + }; +} + +class ButtonMenuRenderer { + List? items; + String? trackingParams; + Accessibility? accessibility; + + ButtonMenuRenderer({ + this.items, + this.trackingParams, + this.accessibility, + }); + + factory ButtonMenuRenderer.fromJson(String str) => ButtonMenuRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ButtonMenuRenderer.fromMap(Map json) => ButtonMenuRenderer( + items: json["items"] == null ? [] : List.from(json["items"]!.map((x) => FluffyItem.fromMap(x))), + trackingParams: json["trackingParams"], + accessibility: json["accessibility"] == null ? null : Accessibility.fromMap(json["accessibility"]), + ); + + Map toMap() => { + "items": items == null ? [] : List.from(items!.map((x) => x.toMap())), + "trackingParams": trackingParams, + "accessibility": accessibility?.toMap(), + }; +} + +class FluffyItem { + MenuItemRenderer? menuNavigationItemRenderer; + MenuItemRenderer? menuServiceItemRenderer; + ToggleMenuServiceItemRenderer? toggleMenuServiceItemRenderer; + + FluffyItem({ + this.menuNavigationItemRenderer, + this.menuServiceItemRenderer, + this.toggleMenuServiceItemRenderer, + }); + + factory FluffyItem.fromJson(String str) => FluffyItem.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory FluffyItem.fromMap(Map json) => FluffyItem( + menuNavigationItemRenderer: json["menuNavigationItemRenderer"] == null ? null : MenuItemRenderer.fromMap(json["menuNavigationItemRenderer"]), + menuServiceItemRenderer: json["menuServiceItemRenderer"] == null ? null : MenuItemRenderer.fromMap(json["menuServiceItemRenderer"]), + toggleMenuServiceItemRenderer: json["toggleMenuServiceItemRenderer"] == null ? null : ToggleMenuServiceItemRenderer.fromMap(json["toggleMenuServiceItemRenderer"]), + ); + + Map toMap() => { + "menuNavigationItemRenderer": menuNavigationItemRenderer?.toMap(), + "menuServiceItemRenderer": menuServiceItemRenderer?.toMap(), + "toggleMenuServiceItemRenderer": toggleMenuServiceItemRenderer?.toMap(), + }; +} + +class ToggleMenuServiceItemRenderer { + Index? defaultText; + Icon? defaultIcon; + DefaultNavigationEndpoint? defaultServiceEndpoint; + Index? toggledText; + Icon? toggledIcon; + ToggledServiceEndpoint? toggledServiceEndpoint; + String? trackingParams; + + ToggleMenuServiceItemRenderer({ + this.defaultText, + this.defaultIcon, + this.defaultServiceEndpoint, + this.toggledText, + this.toggledIcon, + this.toggledServiceEndpoint, + this.trackingParams, + }); + + factory ToggleMenuServiceItemRenderer.fromJson(String str) => ToggleMenuServiceItemRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ToggleMenuServiceItemRenderer.fromMap(Map json) => ToggleMenuServiceItemRenderer( + defaultText: json["defaultText"] == null ? null : Index.fromMap(json["defaultText"]), + defaultIcon: json["defaultIcon"] == null ? null : Icon.fromMap(json["defaultIcon"]), + defaultServiceEndpoint: json["defaultServiceEndpoint"] == null ? null : DefaultNavigationEndpoint.fromMap(json["defaultServiceEndpoint"]), + toggledText: json["toggledText"] == null ? null : Index.fromMap(json["toggledText"]), + toggledIcon: json["toggledIcon"] == null ? null : Icon.fromMap(json["toggledIcon"]), + toggledServiceEndpoint: json["toggledServiceEndpoint"] == null ? null : ToggledServiceEndpoint.fromMap(json["toggledServiceEndpoint"]), + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "defaultText": defaultText?.toMap(), + "defaultIcon": defaultIcon?.toMap(), + "defaultServiceEndpoint": defaultServiceEndpoint?.toMap(), + "toggledText": toggledText?.toMap(), + "toggledIcon": toggledIcon?.toMap(), + "toggledServiceEndpoint": toggledServiceEndpoint?.toMap(), + "trackingParams": trackingParams, + }; +} + +class ToggledServiceEndpoint { + String? clickTrackingParams; + LikeEndpoint? likeEndpoint; + + ToggledServiceEndpoint({ + this.clickTrackingParams, + this.likeEndpoint, + }); + + factory ToggledServiceEndpoint.fromJson(String str) => ToggledServiceEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ToggledServiceEndpoint.fromMap(Map json) => ToggledServiceEndpoint( + clickTrackingParams: json["clickTrackingParams"], + likeEndpoint: json["likeEndpoint"] == null ? null : LikeEndpoint.fromMap(json["likeEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "likeEndpoint": likeEndpoint?.toMap(), + }; +} + +class LikeEndpoint { + String? status; + LikeEndpointTarget? target; + + LikeEndpoint({ + this.status, + this.target, + }); + + factory LikeEndpoint.fromJson(String str) => LikeEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory LikeEndpoint.fromMap(Map json) => LikeEndpoint( + status: json["status"], + target: json["target"] == null ? null : LikeEndpointTarget.fromMap(json["target"]), + ); + + Map toMap() => { + "status": status, + "target": target?.toMap(), + }; +} + +class LikeEndpointTarget { + String? playlistId; + + LikeEndpointTarget({ + this.playlistId, + }); + + factory LikeEndpointTarget.fromJson(String str) => LikeEndpointTarget.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory LikeEndpointTarget.fromMap(Map json) => LikeEndpointTarget( + playlistId: json["playlistId"], + ); + + Map toMap() => { + "playlistId": playlistId, + }; +} + +class ButtonMusicPlayButtonRenderer { + NavigationEndpoint? playNavigationEndpoint; + String? trackingParams; + Icon? playIcon; + Icon? pauseIcon; + int? iconColor; + int? backgroundColor; + int? activeBackgroundColor; + int? loadingIndicatorColor; + Icon? playingIcon; + int? iconLoadingColor; + int? activeScaleFactor; + Accessibility? accessibilityPlayData; + Accessibility? accessibilityPauseData; + + ButtonMusicPlayButtonRenderer({ + this.playNavigationEndpoint, + this.trackingParams, + this.playIcon, + this.pauseIcon, + this.iconColor, + this.backgroundColor, + this.activeBackgroundColor, + this.loadingIndicatorColor, + this.playingIcon, + this.iconLoadingColor, + this.activeScaleFactor, + this.accessibilityPlayData, + this.accessibilityPauseData, + }); + + factory ButtonMusicPlayButtonRenderer.fromJson(String str) => ButtonMusicPlayButtonRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ButtonMusicPlayButtonRenderer.fromMap(Map json) => ButtonMusicPlayButtonRenderer( + playNavigationEndpoint: json["playNavigationEndpoint"] == null ? null : NavigationEndpoint.fromMap(json["playNavigationEndpoint"]), + trackingParams: json["trackingParams"], + playIcon: json["playIcon"] == null ? null : Icon.fromMap(json["playIcon"]), + pauseIcon: json["pauseIcon"] == null ? null : Icon.fromMap(json["pauseIcon"]), + iconColor: json["iconColor"], + backgroundColor: json["backgroundColor"], + activeBackgroundColor: json["activeBackgroundColor"], + loadingIndicatorColor: json["loadingIndicatorColor"], + playingIcon: json["playingIcon"] == null ? null : Icon.fromMap(json["playingIcon"]), + iconLoadingColor: json["iconLoadingColor"], + activeScaleFactor: json["activeScaleFactor"], + accessibilityPlayData: json["accessibilityPlayData"] == null ? null : Accessibility.fromMap(json["accessibilityPlayData"]), + accessibilityPauseData: json["accessibilityPauseData"] == null ? null : Accessibility.fromMap(json["accessibilityPauseData"]), + ); + + Map toMap() => { + "playNavigationEndpoint": playNavigationEndpoint?.toMap(), + "trackingParams": trackingParams, + "playIcon": playIcon?.toMap(), + "pauseIcon": pauseIcon?.toMap(), + "iconColor": iconColor, + "backgroundColor": backgroundColor, + "activeBackgroundColor": activeBackgroundColor, + "loadingIndicatorColor": loadingIndicatorColor, + "playingIcon": playingIcon?.toMap(), + "iconLoadingColor": iconLoadingColor, + "activeScaleFactor": activeScaleFactor, + "accessibilityPlayData": accessibilityPlayData?.toMap(), + "accessibilityPauseData": accessibilityPauseData?.toMap(), + }; +} + +class ButtonToggleButtonRenderer { + bool? isToggled; + bool? isDisabled; + Icon? defaultIcon; + Icon? toggledIcon; + String? trackingParams; + DefaultNavigationEndpoint? defaultNavigationEndpoint; + Accessibility? accessibilityData; + Accessibility? toggledAccessibilityData; + + ButtonToggleButtonRenderer({ + this.isToggled, + this.isDisabled, + this.defaultIcon, + this.toggledIcon, + this.trackingParams, + this.defaultNavigationEndpoint, + this.accessibilityData, + this.toggledAccessibilityData, + }); + + factory ButtonToggleButtonRenderer.fromJson(String str) => ButtonToggleButtonRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ButtonToggleButtonRenderer.fromMap(Map json) => ButtonToggleButtonRenderer( + isToggled: json["isToggled"], + isDisabled: json["isDisabled"], + defaultIcon: json["defaultIcon"] == null ? null : Icon.fromMap(json["defaultIcon"]), + toggledIcon: json["toggledIcon"] == null ? null : Icon.fromMap(json["toggledIcon"]), + trackingParams: json["trackingParams"], + defaultNavigationEndpoint: json["defaultNavigationEndpoint"] == null ? null : DefaultNavigationEndpoint.fromMap(json["defaultNavigationEndpoint"]), + accessibilityData: json["accessibilityData"] == null ? null : Accessibility.fromMap(json["accessibilityData"]), + toggledAccessibilityData: json["toggledAccessibilityData"] == null ? null : Accessibility.fromMap(json["toggledAccessibilityData"]), + ); + + Map toMap() => { + "isToggled": isToggled, + "isDisabled": isDisabled, + "defaultIcon": defaultIcon?.toMap(), + "toggledIcon": toggledIcon?.toMap(), + "trackingParams": trackingParams, + "defaultNavigationEndpoint": defaultNavigationEndpoint?.toMap(), + "accessibilityData": accessibilityData?.toMap(), + "toggledAccessibilityData": toggledAccessibilityData?.toMap(), + }; +} + +class MusicResponsiveHeaderRendererDescription { + MusicDescriptionShelfRenderer? musicDescriptionShelfRenderer; + + MusicResponsiveHeaderRendererDescription({ + this.musicDescriptionShelfRenderer, + }); + + factory MusicResponsiveHeaderRendererDescription.fromJson(String str) => MusicResponsiveHeaderRendererDescription.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicResponsiveHeaderRendererDescription.fromMap(Map json) => MusicResponsiveHeaderRendererDescription( + musicDescriptionShelfRenderer: json["musicDescriptionShelfRenderer"] == null ? null : MusicDescriptionShelfRenderer.fromMap(json["musicDescriptionShelfRenderer"]), + ); + + Map toMap() => { + "musicDescriptionShelfRenderer": musicDescriptionShelfRenderer?.toMap(), + }; +} + +class MusicDescriptionShelfRenderer { + MusicDescriptionShelfRendererDescription? description; + MoreButton? moreButton; + String? trackingParams; + String? shelfStyle; + + MusicDescriptionShelfRenderer({ + this.description, + this.moreButton, + this.trackingParams, + this.shelfStyle, + }); + + factory MusicDescriptionShelfRenderer.fromJson(String str) => MusicDescriptionShelfRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicDescriptionShelfRenderer.fromMap(Map json) => MusicDescriptionShelfRenderer( + description: json["description"] == null ? null : MusicDescriptionShelfRendererDescription.fromMap(json["description"]), + moreButton: json["moreButton"] == null ? null : MoreButton.fromMap(json["moreButton"]), + trackingParams: json["trackingParams"], + shelfStyle: json["shelfStyle"], + ); + + Map toMap() => { + "description": description?.toMap(), + "moreButton": moreButton?.toMap(), + "trackingParams": trackingParams, + "shelfStyle": shelfStyle, + }; +} + +class MusicDescriptionShelfRendererDescription { + List? runs; + + MusicDescriptionShelfRendererDescription({ + this.runs, + }); + + factory MusicDescriptionShelfRendererDescription.fromJson(String str) => MusicDescriptionShelfRendererDescription.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicDescriptionShelfRendererDescription.fromMap(Map json) => MusicDescriptionShelfRendererDescription( + runs: json["runs"] == null ? [] : List.from(json["runs"]!.map((x) => DescriptionRun.fromMap(x))), + ); + + Map toMap() => { + "runs": runs == null ? [] : List.from(runs!.map((x) => x.toMap())), + }; +} + +class DescriptionRun { + String? text; + PurpleNavigationEndpoint? navigationEndpoint; + + DescriptionRun({ + this.text, + this.navigationEndpoint, + }); + + factory DescriptionRun.fromJson(String str) => DescriptionRun.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory DescriptionRun.fromMap(Map json) => DescriptionRun( + text: json["text"], + navigationEndpoint: json["navigationEndpoint"] == null ? null : PurpleNavigationEndpoint.fromMap(json["navigationEndpoint"]), + ); + + Map toMap() => { + "text": text, + "navigationEndpoint": navigationEndpoint?.toMap(), + }; +} + +class PurpleNavigationEndpoint { + String? clickTrackingParams; + UrlEndpoint? urlEndpoint; + + PurpleNavigationEndpoint({ + this.clickTrackingParams, + this.urlEndpoint, + }); + + factory PurpleNavigationEndpoint.fromJson(String str) => PurpleNavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PurpleNavigationEndpoint.fromMap(Map json) => PurpleNavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + urlEndpoint: json["urlEndpoint"] == null ? null : UrlEndpoint.fromMap(json["urlEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "urlEndpoint": urlEndpoint?.toMap(), + }; +} + +class UrlEndpoint { + String? url; + String? target; + + UrlEndpoint({ + this.url, + this.target, + }); + + factory UrlEndpoint.fromJson(String str) => UrlEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory UrlEndpoint.fromMap(Map json) => UrlEndpoint( + url: json["url"], + target: json["target"], + ); + + Map toMap() => { + "url": url, + "target": target, + }; +} + +class MoreButton { + MoreButtonToggleButtonRenderer? toggleButtonRenderer; + + MoreButton({ + this.toggleButtonRenderer, + }); + + factory MoreButton.fromJson(String str) => MoreButton.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MoreButton.fromMap(Map json) => MoreButton( + toggleButtonRenderer: json["toggleButtonRenderer"] == null ? null : MoreButtonToggleButtonRenderer.fromMap(json["toggleButtonRenderer"]), + ); + + Map toMap() => { + "toggleButtonRenderer": toggleButtonRenderer?.toMap(), + }; +} + +class MoreButtonToggleButtonRenderer { + bool? isToggled; + bool? isDisabled; + Icon? defaultIcon; + Index? defaultText; + Icon? toggledIcon; + Index? toggledText; + String? trackingParams; + + MoreButtonToggleButtonRenderer({ + this.isToggled, + this.isDisabled, + this.defaultIcon, + this.defaultText, + this.toggledIcon, + this.toggledText, + this.trackingParams, + }); + + factory MoreButtonToggleButtonRenderer.fromJson(String str) => MoreButtonToggleButtonRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MoreButtonToggleButtonRenderer.fromMap(Map json) => MoreButtonToggleButtonRenderer( + isToggled: json["isToggled"], + isDisabled: json["isDisabled"], + defaultIcon: json["defaultIcon"] == null ? null : Icon.fromMap(json["defaultIcon"]), + defaultText: json["defaultText"] == null ? null : Index.fromMap(json["defaultText"]), + toggledIcon: json["toggledIcon"] == null ? null : Icon.fromMap(json["toggledIcon"]), + toggledText: json["toggledText"] == null ? null : Index.fromMap(json["toggledText"]), + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "isToggled": isToggled, + "isDisabled": isDisabled, + "defaultIcon": defaultIcon?.toMap(), + "defaultText": defaultText?.toMap(), + "toggledIcon": toggledIcon?.toMap(), + "toggledText": toggledText?.toMap(), + "trackingParams": trackingParams, + }; +} + +class StraplineTextOne { + List? runs; + + StraplineTextOne({ + this.runs, + }); + + factory StraplineTextOne.fromJson(String str) => StraplineTextOne.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory StraplineTextOne.fromMap(Map json) => StraplineTextOne( + runs: json["runs"] == null ? [] : List.from(json["runs"]!.map((x) => StraplineTextOneRun.fromMap(x))), + ); + + Map toMap() => { + "runs": runs == null ? [] : List.from(runs!.map((x) => x.toMap())), + }; +} + +class StraplineTextOneRun { + String? text; + FluffyNavigationEndpoint? navigationEndpoint; + + StraplineTextOneRun({ + this.text, + this.navigationEndpoint, + }); + + factory StraplineTextOneRun.fromJson(String str) => StraplineTextOneRun.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory StraplineTextOneRun.fromMap(Map json) => StraplineTextOneRun( + text: json["text"], + navigationEndpoint: json["navigationEndpoint"] == null ? null : FluffyNavigationEndpoint.fromMap(json["navigationEndpoint"]), + ); + + Map toMap() => { + "text": text, + "navigationEndpoint": navigationEndpoint?.toMap(), + }; +} + +class FluffyNavigationEndpoint { + String? clickTrackingParams; + BrowseEndpoint? browseEndpoint; + + FluffyNavigationEndpoint({ + this.clickTrackingParams, + this.browseEndpoint, + }); + + factory FluffyNavigationEndpoint.fromJson(String str) => FluffyNavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory FluffyNavigationEndpoint.fromMap(Map json) => FluffyNavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + browseEndpoint: json["browseEndpoint"] == null ? null : BrowseEndpoint.fromMap(json["browseEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "browseEndpoint": browseEndpoint?.toMap(), + }; +} + +class Microformat { + MicroformatDataRenderer? microformatDataRenderer; + + Microformat({ + this.microformatDataRenderer, + }); + + factory Microformat.fromJson(String str) => Microformat.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Microformat.fromMap(Map json) => Microformat( + microformatDataRenderer: json["microformatDataRenderer"] == null ? null : MicroformatDataRenderer.fromMap(json["microformatDataRenderer"]), + ); + + Map toMap() => { + "microformatDataRenderer": microformatDataRenderer?.toMap(), + }; +} + +class MicroformatDataRenderer { + String? urlCanonical; + + MicroformatDataRenderer({ + this.urlCanonical, + }); + + factory MicroformatDataRenderer.fromJson(String str) => MicroformatDataRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MicroformatDataRenderer.fromMap(Map json) => MicroformatDataRenderer( + urlCanonical: json["urlCanonical"], + ); + + Map toMap() => { + "urlCanonical": urlCanonical, + }; +} + +class ResponseContext { + String? visitorData; + List? serviceTrackingParams; + + ResponseContext({ + this.visitorData, + this.serviceTrackingParams, + }); + + factory ResponseContext.fromJson(String str) => ResponseContext.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ResponseContext.fromMap(Map json) => ResponseContext( + visitorData: json["visitorData"], + serviceTrackingParams: json["serviceTrackingParams"] == null ? [] : List.from(json["serviceTrackingParams"]!.map((x) => ServiceTrackingParam.fromMap(x))), + ); + + Map toMap() => { + "visitorData": visitorData, + "serviceTrackingParams": serviceTrackingParams == null ? [] : List.from(serviceTrackingParams!.map((x) => x.toMap())), + }; +} + +class ServiceTrackingParam { + String? service; + List? params; + + ServiceTrackingParam({ + this.service, + this.params, + }); + + factory ServiceTrackingParam.fromJson(String str) => ServiceTrackingParam.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ServiceTrackingParam.fromMap(Map json) => ServiceTrackingParam( + service: json["service"], + params: json["params"] == null ? [] : List.from(json["params"]!.map((x) => Param.fromMap(x))), + ); + + Map toMap() => { + "service": service, + "params": params == null ? [] : List.from(params!.map((x) => x.toMap())), + }; +} + +class Param { + String? key; + String? value; + + Param({ + this.key, + this.value, + }); + + factory Param.fromJson(String str) => Param.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Param.fromMap(Map json) => Param( + key: json["key"], + value: json["value"], + ); + + Map toMap() => { + "key": key, + "value": value, + }; +} diff --git a/lib/data/sideb/models/browse_model.dart b/lib/data/models/browse_model.dart similarity index 99% rename from lib/data/sideb/models/browse_model.dart rename to lib/data/models/browse_model.dart index 0291284..cbae2e8 100644 --- a/lib/data/sideb/models/browse_model.dart +++ b/lib/data/models/browse_model.dart @@ -1,6 +1,6 @@ // Author: fengshengxiong // Date: 2024/6/18 -// Description: Browse接口模型 +// Description: Browse-首页模型 import 'dart:convert'; @@ -550,10 +550,12 @@ class PurpleNavigationEndpoint { class PurpleBrowseEndpoint { String? browseId; + String? params; BrowseEndpointContextSupportedConfigs? browseEndpointContextSupportedConfigs; PurpleBrowseEndpoint({ this.browseId, + this.params, this.browseEndpointContextSupportedConfigs, }); @@ -563,11 +565,13 @@ class PurpleBrowseEndpoint { factory PurpleBrowseEndpoint.fromMap(Map json) => PurpleBrowseEndpoint( browseId: json["browseId"], + params: json["params"], browseEndpointContextSupportedConfigs: json["browseEndpointContextSupportedConfigs"] == null ? null : BrowseEndpointContextSupportedConfigs.fromMap(json["browseEndpointContextSupportedConfigs"]), ); Map toMap() => { "browseId": browseId, + "params": params, "browseEndpointContextSupportedConfigs": browseEndpointContextSupportedConfigs?.toMap(), }; } diff --git a/lib/data/sideb/models/home_model.dart b/lib/data/models/home_model.dart similarity index 95% rename from lib/data/sideb/models/home_model.dart rename to lib/data/models/home_model.dart index da5cf3f..cd8ecde 100644 --- a/lib/data/sideb/models/home_model.dart +++ b/lib/data/models/home_model.dart @@ -39,6 +39,7 @@ class Content { String? videoId; String? playlistId; String? browseId; + String? params; Content({ this.title, @@ -47,6 +48,7 @@ class Content { this.videoId, this.playlistId, this.browseId, + this.params, }); factory Content.fromJson(String str) => Content.fromMap(json.decode(str)); @@ -60,6 +62,7 @@ class Content { videoId: json["videoId"], playlistId: json["playlistId"], browseId: json["browseId"], + params: json["params"], ); Map toMap() => { @@ -69,5 +72,6 @@ class Content { "videoId": videoId, "playlistId": playlistId, "browseId": browseId, + "params": params, }; } diff --git a/lib/data/sideb/models/isocode_model.dart b/lib/data/models/isocode_model.dart similarity index 100% rename from lib/data/sideb/models/isocode_model.dart rename to lib/data/models/isocode_model.dart diff --git a/lib/data/models/music_model.dart b/lib/data/models/music_model.dart new file mode 100644 index 0000000..e3262a9 --- /dev/null +++ b/lib/data/models/music_model.dart @@ -0,0 +1,74 @@ +// Author: fengshengxiong +// Date: 2024/6/24 +// Description: 音乐模型 + +import 'dart:convert'; + +import 'package:hive/hive.dart'; + +part 'music_model.g.dart'; + +@HiveType(typeId: 2) +class MusicModel extends HiveObject { + @HiveField(0) + String? title; + @HiveField(1) + String? subTitle; + @HiveField(2) + String? thumbnail; + @HiveField(3) + String? videoId; + @HiveField(4) + String? playlistId; + @HiveField(5) + String? url; + + MusicModel({ + this.title, + this.subTitle, + this.thumbnail, + this.videoId, + this.playlistId, + this.url, + }); + + MusicModel copyWith({ + String? title, + String? subTitle, + String? thumbnail, + String? url, + String? videoId, + String? playlistId, + }) => + MusicModel( + title: title ?? this.title, + subTitle: subTitle ?? this.subTitle, + thumbnail: thumbnail ?? this.thumbnail, + url: url ?? this.url, + videoId: videoId ?? this.videoId, + playlistId: playlistId ?? this.playlistId, + ); + + factory MusicModel.fromJson(String str) => + MusicModel.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicModel.fromMap(Map json) => MusicModel( + title: json["title"], + subTitle: json["subTitle"], + thumbnail: json["thumbnail"], + videoId: json["videoId"], + playlistId: json["playlistId"], + url: json["url"], + ); + + Map toMap() => { + "title": title, + "subTitle": subTitle, + "thumbnail": thumbnail, + "videoId": videoId, + "playlistId": playlistId, + "url": url, + }; +} diff --git a/lib/data/models/music_model.g.dart b/lib/data/models/music_model.g.dart new file mode 100644 index 0000000..4aa388f --- /dev/null +++ b/lib/data/models/music_model.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'music_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class MusicModelAdapter extends TypeAdapter { + @override + final int typeId = 2; + + @override + MusicModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return MusicModel( + title: fields[0] as String?, + subTitle: fields[1] as String?, + thumbnail: fields[2] as String?, + videoId: fields[3] as String?, + playlistId: fields[4] as String?, + url: fields[5] as String?, + ); + } + + @override + void write(BinaryWriter writer, MusicModel obj) { + writer + ..writeByte(6) + ..writeByte(0) + ..write(obj.title) + ..writeByte(1) + ..write(obj.subTitle) + ..writeByte(2) + ..write(obj.thumbnail) + ..writeByte(3) + ..write(obj.videoId) + ..writeByte(4) + ..write(obj.playlistId) + ..writeByte(5) + ..write(obj.url); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MusicModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/sideb/models/next_model.dart b/lib/data/models/next_model.dart similarity index 100% rename from lib/data/sideb/models/next_model.dart rename to lib/data/models/next_model.dart diff --git a/lib/data/sideb/models/player_model.dart b/lib/data/models/player_model.dart similarity index 100% rename from lib/data/sideb/models/player_model.dart rename to lib/data/models/player_model.dart diff --git a/lib/data/sidea/models/voice_model.dart b/lib/data/models/voice_model.dart similarity index 100% rename from lib/data/sidea/models/voice_model.dart rename to lib/data/models/voice_model.dart diff --git a/lib/data/sidea/models/voice_model.g.dart b/lib/data/models/voice_model.g.dart similarity index 100% rename from lib/data/sidea/models/voice_model.g.dart rename to lib/data/models/voice_model.g.dart diff --git a/lib/data/network/dio_client.dart b/lib/data/network/dio_client.dart index ddb341e..f49ecf9 100644 --- a/lib/data/network/dio_client.dart +++ b/lib/data/network/dio_client.dart @@ -7,7 +7,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:tone_snap/components/base_easyloading.dart'; -import 'package:tone_snap/data/sideb/api/music_api.dart'; +import 'package:tone_snap/data/api/music_api.dart'; import 'package:tone_snap/data/network/base_error.dart'; import 'package:tone_snap/data/network/dio_interceptor.dart'; import 'package:tone_snap/utils/log_util.dart'; @@ -61,14 +61,14 @@ class DioClient { /// 请求 Future request( String path, { + bool showLoading = false, + bool showToast = false, required RequestMethod requestMethod, dynamic data, Map? queryParameters, CancelToken? cancelToken, Options? options, bool isFormData = false, - bool showLoading = false, - bool showToast = false, T Function(Map)? formJson, required Function(T? result) success, Function(BaseError baseError)? fail, @@ -94,7 +94,7 @@ class DioClient { } catch (e) { BaseError baseError = getError(e); if (fail != null) fail(baseError); - BaseEasyLoading.toast(baseError.message); + BaseEasyLoading.toast(baseError.message, show: showToast); LogUtil.e(e.toString()); } } diff --git a/lib/data/sideb/models/play_list_model.dart b/lib/data/sideb/models/play_list_model.dart deleted file mode 100644 index ba635fc..0000000 --- a/lib/data/sideb/models/play_list_model.dart +++ /dev/null @@ -1,45 +0,0 @@ -// Author: fengshengxiong -// Date: 2024/6/24 -// Description: 播放列表模型 - -import 'dart:convert'; - -class PlayListModel { - String? title; - String? subTitle; - String? thumbnail; - String? videoId; - String? playlistId; - String? url; - - PlayListModel({ - this.title, - this.subTitle, - this.thumbnail, - this.videoId, - this.playlistId, - this.url, - }); - - factory PlayListModel.fromJson(String str) => PlayListModel.fromMap(json.decode(str)); - - String toJson() => json.encode(toMap()); - - factory PlayListModel.fromMap(Map json) => PlayListModel( - title: json["title"], - subTitle: json["subTitle"], - thumbnail: json["thumbnail"], - videoId: json["videoId"], - playlistId: json["playlistId"], - url: json["url"], - ); - - Map toMap() => { - "title": title, - "subTitle": subTitle, - "thumbnail": thumbnail, - "videoId": videoId, - "playlistId": playlistId, - "url": url, - }; -} diff --git a/lib/data/sidea/storage/favorite_data.dart b/lib/data/storage/favorite_box.dart similarity index 81% rename from lib/data/sidea/storage/favorite_data.dart rename to lib/data/storage/favorite_box.dart index baca822..6ba3837 100644 --- a/lib/data/sidea/storage/favorite_data.dart +++ b/lib/data/storage/favorite_box.dart @@ -2,18 +2,18 @@ // Date: 2024/5/8 // Description: 收藏数据 -import 'package:tone_snap/data/sidea/models/voice_model.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; import 'package:tone_snap/data/storage/hive_storage.dart'; -class FavoriteData { +class FavoriteBox { /// 私有构造函数 - FavoriteData._(); + FavoriteBox._(); /// 静态常量用于保存类的唯一实例 - static final FavoriteData _instance = FavoriteData._(); + static final FavoriteBox _instance = FavoriteBox._(); /// 工厂构造函数返回类的唯一实例 - factory FavoriteData() { + factory FavoriteBox() { return _instance; } diff --git a/lib/data/storage/hive_storage.dart b/lib/data/storage/hive_storage.dart index 0449b77..08164d2 100644 --- a/lib/data/storage/hive_storage.dart +++ b/lib/data/storage/hive_storage.dart @@ -3,19 +3,27 @@ // Description: 持久化储存 import 'package:hive_flutter/hive_flutter.dart'; -import 'package:tone_snap/data/sidea/models/voice_model.dart'; +import 'package:tone_snap/data/enum/play_mode.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; const myVoiceBox = 'myVoiceBox'; const favoriteBox = 'favoriteBox'; +const musicBox = 'musicBox'; +const loveSongsBox = 'loveSongsBox'; Future initHive() async { // 初始化 await Hive.initFlutter(); // 注册类型适配器 Hive.registerAdapter(VoiceModelAdapter()); + Hive.registerAdapter(PlayModeAdapter()); + Hive.registerAdapter(MusicModelAdapter()); // 打开盒子 await Hive.openBox(myVoiceBox); await Hive.openBox(favoriteBox); + await Hive.openBox(musicBox); + await Hive.openBox(loveSongsBox); } Box getMyVoiceBox() { @@ -24,4 +32,12 @@ Box getMyVoiceBox() { Box getFavoriteBox() { return Hive.box(favoriteBox); +} + +Box getMusicBox() { + return Hive.box(musicBox); +} + +Box getLoveSongsBox() { + return Hive.box(loveSongsBox); } \ No newline at end of file diff --git a/lib/data/storage/love_songs_box.dart b/lib/data/storage/love_songs_box.dart new file mode 100644 index 0000000..5035212 --- /dev/null +++ b/lib/data/storage/love_songs_box.dart @@ -0,0 +1,53 @@ +// Author: fengshengxiong +// Date: 2024/5/8 +// Description: 喜欢的歌曲 + +import 'package:get/get.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/storage/hive_storage.dart'; + +class LoveSongsBox { + LoveSongsBox._(); + + static final LoveSongsBox _instance = LoveSongsBox._(); + + factory LoveSongsBox() { + return _instance; + } + + /// 声明盒子 + /// 注意, main函数中这个盒子已经打开, 可以进行存储操作 + final _box = getLoveSongsBox(); + + /// 获取数据 + List getList() { + return _box.values.toList(); + } + + /// 添加数据 + Future addData(MusicModel model) async { + return await _box.add(model); + } + + /// 校验该歌曲是否加入喜欢 + bool isLove(String videoId) { + var model = getList().firstWhereOrNull((e) => e.videoId == videoId); + return model != null; + } + + /// 删除 + Future delete(String videoId) async { + var list = getList(); + var model = list.firstWhereOrNull((e) => e.videoId == videoId); + if (model != null) { + await _box.delete(list.indexOf(model)); + await _box.flush(); + } + } + + /// 清空所有数据 + Future clear() async { + await _box.clear(); + await _box.flush(); + } +} diff --git a/lib/data/storage/music_box.dart b/lib/data/storage/music_box.dart new file mode 100644 index 0000000..1072d29 --- /dev/null +++ b/lib/data/storage/music_box.dart @@ -0,0 +1,30 @@ +// Author: fengshengxiong +// Date: 2024/5/8 +// Description: 音乐数据盒子 + +import 'package:tone_snap/data/enum/play_mode.dart'; +import 'package:tone_snap/data/storage/hive_storage.dart'; + +class MusicBox { + MusicBox._(); + + static final MusicBox _instance = MusicBox._(); + + factory MusicBox() { + return _instance; + } + + /// 声明盒子 + /// 注意, main函数中这个盒子已经打开, 可以进行存储操作 + final _box = getMusicBox(); + + /// 设置播放模式 + Future putPlayMode(PlayMode playMode) { + return _box.put('play_mode', playMode); + } + + /// 获取播放模式 + PlayMode getPlayMode() { + return _box.get('play_mode') ?? PlayMode.listLoop; + } +} diff --git a/lib/data/sidea/storage/my_voice_data.dart b/lib/data/storage/my_voice_box.dart similarity index 79% rename from lib/data/sidea/storage/my_voice_data.dart rename to lib/data/storage/my_voice_box.dart index 0cd0460..9ea698f 100644 --- a/lib/data/sidea/storage/my_voice_data.dart +++ b/lib/data/storage/my_voice_box.dart @@ -1,19 +1,19 @@ // Author: fengshengxiong // Date: 2024/5/8 -// Description: 收藏数据 +// Description: 我的音频 -import 'package:tone_snap/data/sidea/models/voice_model.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; import 'package:tone_snap/data/storage/hive_storage.dart'; -class MyVoiceData { +class MyVoiceBox { /// 私有构造函数 - MyVoiceData._(); + MyVoiceBox._(); /// 静态常量用于保存类的唯一实例 - static final MyVoiceData _instance = MyVoiceData._(); + static final MyVoiceBox _instance = MyVoiceBox._(); /// 工厂构造函数返回类的唯一实例 - factory MyVoiceData() { + factory MyVoiceBox() { return _instance; } diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index 0d155ab..75bdb46 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -63,9 +63,12 @@ class Assets { static const String sideAUploadRecordSound = 'assets/images/side_a/upload_record_sound.png'; static const String sideAUserAgreement = 'assets/images/side_a/user_agreement.png'; static const String sideAVoiceDefault = 'assets/images/side_a/voice_default.png'; + static const String sideBAlbumTitleBg = 'assets/images/side_b/album_title_bg.png'; + static const String sideBAlbumTotal = 'assets/images/side_b/album_total.png'; static const String sideBArrowDownBack = 'assets/images/side_b/arrow_down_back.png'; static const String sideBArrowLeftBack = 'assets/images/side_b/arrow_left_back.png'; static const String sideBArrowRightItem = 'assets/images/side_b/arrow_right_item.png'; + static const String sideBArtists = 'assets/images/side_b/artists.png'; static const String sideBBnb1Selected = 'assets/images/side_b/bnb1_selected.png'; static const String sideBBnb1Unselected = 'assets/images/side_b/bnb1_unselected.png'; static const String sideBBnb2Selected = 'assets/images/side_b/bnb2_selected.png'; @@ -73,20 +76,28 @@ class Assets { static const String sideBBnb3Selected = 'assets/images/side_b/bnb3_selected.png'; static const String sideBBnb3Unselected = 'assets/images/side_b/bnb3_unselected.png'; static const String sideBBnbBg = 'assets/images/side_b/bnb_bg.png'; + static const String sideBBottomSheetIndicator = 'assets/images/side_b/bottom_sheet_indicator.png'; + static const String sideBCollectionAlbum = 'assets/images/side_b/collection_album.png'; static const String sideBCrossCircle = 'assets/images/side_b/cross_circle.png'; static const String sideBDownload = 'assets/images/side_b/download.png'; static const String sideBHomeBg = 'assets/images/side_b/home_bg.png'; static const String sideBImgError = 'assets/images/side_b/img_error.png'; static const String sideBImgPlaceholder = 'assets/images/side_b/img_placeholder.png'; static const String sideBItemPlayer1 = 'assets/images/side_b/item_player1.png'; - static const String sideBLaunchImage = 'assets/images/side_b/launch_image.png'; static const String sideBLineMenu = 'assets/images/side_b/line_menu.png'; static const String sideBListLoop = 'assets/images/side_b/list_loop.png'; static const String sideBLove = 'assets/images/side_b/love.png'; - static const String sideBNextSong = 'assets/images/side_b/next_song.png'; + static const String sideBLoveSolid = 'assets/images/side_b/love_solid.png'; + static const String sideBNextTrack = 'assets/images/side_b/next_track.png'; + static const String sideBNotCollectionAlbum = 'assets/images/side_b/not_collection_album.png'; + static const String sideBOfflineDownload = 'assets/images/side_b/offline_download.png'; static const String sideBPausePlay = 'assets/images/side_b/pause_play.png'; + static const String sideBPersonalMusicLibraryBg = 'assets/images/side_b/personal_music_library_bg.png'; + static const String sideBPlaceholderLibrary = 'assets/images/side_b/placeholder_library.png'; static const String sideBPlayList = 'assets/images/side_b/play_list.png'; - static const String sideBPreviousSong = 'assets/images/side_b/previous_song.png'; + static const String sideBPlayListDelete = 'assets/images/side_b/play_list_delete.png'; + static const String sideBPlaylistsAdd = 'assets/images/side_b/playlists_add.png'; + static const String sideBPreviousTrack = 'assets/images/side_b/previous_track.png'; static const String sideBShufflePlayback = 'assets/images/side_b/shuffle_playback.png'; static const String sideBSingleCycle = 'assets/images/side_b/single_cycle.png'; static const String sideBStartPlay = 'assets/images/side_b/start_play.png'; diff --git a/lib/data/app_config.dart b/lib/global/app_config.dart similarity index 75% rename from lib/data/app_config.dart rename to lib/global/app_config.dart index e862f09..f9cf59f 100644 --- a/lib/data/app_config.dart +++ b/lib/global/app_config.dart @@ -5,8 +5,10 @@ import 'package:tone_snap/data/enum/app_side_enum.dart'; class AppConfig { + static const appName = 'ToneSnap'; + /// 当前App展示的一面 - static const AppSideEnum appSideEnum = AppSideEnum.sideA; + static const AppSideEnum appSideEnum = AppSideEnum.sideB; /// 默认语言环境和所在区域 static String defaultLocale = 'zh-CN'; diff --git a/lib/global/app_tracking_transparency_manager.dart b/lib/global/app_tracking_transparency_manager.dart new file mode 100644 index 0000000..6342e2c --- /dev/null +++ b/lib/global/app_tracking_transparency_manager.dart @@ -0,0 +1,51 @@ +// Author: fengshengxiong +// Date: 2024/6/26 +// Description: 应用程序跟踪透明度管理器 + +import 'dart:async'; +import 'dart:io'; + +import 'package:app_tracking_transparency/app_tracking_transparency.dart'; +import 'package:tone_snap/utils/log_util.dart'; + +class AppTrackingTransparencyManager { + AppTrackingTransparencyManager._(); + + static final AppTrackingTransparencyManager _instance = AppTrackingTransparencyManager._(); + + factory AppTrackingTransparencyManager() { + return _instance; + } + + Timer? _timer; + + /// 请求跟踪授权 + Future requestATT() async { + if (Platform.isIOS) { + final TrackingStatus status = await AppTrackingTransparency.trackingAuthorizationStatus; + if (status == TrackingStatus.notDetermined) { + if (_timer != null && _timer!.isActive) { + final TrackingStatus status = await AppTrackingTransparency.requestTrackingAuthorization(); + LogUtil.d('跟踪授权状态: $status'); + } else { + _startTimer(); + } + } else { + _stopTimer(); + } + } + } + + /// 开始定时器 + void _startTimer() { + _timer = Timer.periodic(const Duration(seconds: 3), (Timer t) { + requestATT(); + }); + } + + /// 停止定时器 + void _stopTimer() { + _timer?.cancel(); + _timer = null; + } +} diff --git a/lib/main.dart b/lib/main.dart index ff1f04b..88291ce 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,13 +10,15 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; import 'package:tone_snap/components/base_easyloading.dart'; -import 'package:tone_snap/data/app_config.dart'; +import 'package:tone_snap/components/music_bar.dart'; +import 'package:tone_snap/components/music_bar/music_bar_controller.dart'; import 'package:tone_snap/data/enum/app_side_enum.dart'; import 'package:tone_snap/data/storage/hive_storage.dart'; import 'package:tone_snap/firebase/firebase_options.dart'; +import 'package:tone_snap/global/app_config.dart'; import 'package:tone_snap/global/network_connectivity_service.dart'; +import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart'; import 'package:tone_snap/res/themes/app_themes.dart'; -import 'package:tone_snap/res/values/strings.dart'; import 'package:tone_snap/routes/app_pages.dart'; import 'package:tone_snap/routes/app_routes.dart'; import 'package:tone_snap/utils/file_util.dart'; @@ -26,35 +28,37 @@ import 'package:tone_snap/utils/log_util.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - // 初始化Firebase - try { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); - } catch (e) { - LogUtil.e("Firebase initialization error: $e"); + if (Platform.isIOS) { + // 初始化Firebase + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } catch (e) { + LogUtil.e("Firebase initialization error: $e"); + } + + // 非异步错误 + FlutterError.onError = (errorDetails) { + FirebaseCrashlytics.instance.recordFlutterError(errorDetails); + }; + + // 异步错误 + PlatformDispatcher.instance.onError = (error, stack) { + FirebaseCrashlytics.instance.recordError(error, stack, fatal: false); + return true; + }; + + // 初始化广告 SDK + MobileAds.instance.initialize(); + + // 监听网络变化 + await Get.putAsync(() async => NetworkConnectivityService()); } - // 非异步错误 - FlutterError.onError = (errorDetails) { - FirebaseCrashlytics.instance.recordFlutterError(errorDetails); - }; - - // 异步错误 - PlatformDispatcher.instance.onError = (error, stack) { - FirebaseCrashlytics.instance.recordError(error, stack, fatal: false); - return true; - }; - - // 初始化广告 SDK - MobileAds.instance.initialize(); - // 初始化Hive await initHive(); - // 监听网络变化 - await Get.putAsync(() async => NetworkConnectivityService()); - runApp(const MyApp()); // 竖屏 @@ -83,44 +87,54 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { final easyLoading = EasyLoading.init(); + ThemeData appTheme; + List navigatorObservers = const []; + if (AppConfig.appSideEnum == AppSideEnum.sideA) { + appTheme = sideATheme; + } else { + appTheme = sideBTheme; + navigatorObservers = [ + GetObserver((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (Get.currentRoute == AppRoutes.playPage) { + MusicBar().hide(); + } else { + if (Get.isRegistered() && MusicPlayerController.to.musicModel.value.videoId != null) { + MusicBar().show(); + } + } + if (Get.isRegistered()) { + if (Get.currentRoute == AppRoutes.initialB) { + MusicBarController.to.riseUp(); + } else { + MusicBarController.to.toBottom(); + } + } + }); + }), + ]; + } return ScreenUtilInit( // 以设计稿的尺寸为基准进行适配 designSize: const Size(375, 812), minTextAdapt: true, builder: (context, child) { - if (AppConfig.appSideEnum == AppSideEnum.sideA) { - return GetMaterialApp( - title: appName, - debugShowCheckedModeBanner: false, - theme: sideATheme, - darkTheme: sideATheme, - themeMode: ThemeMode.dark, - initialRoute: AppRoutes.splashA, - getPages: AppPages.routes, - builder: (context, widget) { - BaseEasyLoading.initGlobalConfig(); - widget = easyLoading(context, widget); - // 设置文字大小不随系统设置改变 - return MediaQuery.withNoTextScaling(child: widget); - }, - ); - } else { - return GetMaterialApp( - title: appName, - debugShowCheckedModeBanner: false, - theme: sideBTheme, - darkTheme: sideBTheme, - themeMode: ThemeMode.dark, - initialRoute: AppRoutes.splashB, - getPages: AppPages.routes, - builder: (context, widget) { - BaseEasyLoading.initGlobalConfig(); - widget = easyLoading(context, widget); - // 设置文字大小不随系统设置改变 - return MediaQuery.withNoTextScaling(child: widget); - }, - ); - } + return GetMaterialApp( + title: AppConfig.appName, + debugShowCheckedModeBanner: false, + theme: appTheme, + darkTheme: appTheme, + themeMode: ThemeMode.dark, + initialRoute: AppRoutes.splash, + getPages: AppPages.routes, + navigatorObservers: navigatorObservers, + builder: (context, widget) { + BaseEasyLoading.configLoading(); + widget = easyLoading(context, widget); + // 设置文字大小不随系统设置改变 + return MediaQuery.withNoTextScaling(child: widget); + }, + ); }, ); } diff --git a/lib/modules/sidea/about/about_view.dart b/lib/modules/sidea/about/about_view.dart index 92c4bc0..323362b 100644 --- a/lib/modules/sidea/about/about_view.dart +++ b/lib/modules/sidea/about/about_view.dart @@ -3,8 +3,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:tone_snap/components/base_appbar.dart'; import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/global/app_config.dart'; import 'package:tone_snap/modules/sidea/about/about_controller.dart'; -import 'package:tone_snap/res/values/strings.dart'; class AboutView extends StatelessWidget { AboutView({super.key}); @@ -32,7 +32,7 @@ class AboutView extends StatelessWidget { overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, text: TextSpan( - text: appName, + text: AppConfig.appName, style: TextStyle( color: Colors.white, fontSize: 22.sp, diff --git a/lib/modules/sidea/change_voice/change_voice_controller.dart b/lib/modules/sidea/change_voice/change_voice_controller.dart index 7e78d13..dc4600e 100644 --- a/lib/modules/sidea/change_voice/change_voice_controller.dart +++ b/lib/modules/sidea/change_voice/change_voice_controller.dart @@ -1,11 +1,13 @@ +import 'dart:io'; + import 'package:ffmpeg_kit_flutter_audio/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter_audio/ffmpeg_session.dart'; import 'package:ffmpeg_kit_flutter_audio/return_code.dart'; import 'package:get/get.dart'; import 'package:tone_snap/ads/interstitial_ad_manager.dart'; import 'package:tone_snap/components/base_easyloading.dart'; -import 'package:tone_snap/data/sidea/models/voice_model.dart'; -import 'package:tone_snap/data/sidea/storage/my_voice_data.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/data/storage/my_voice_box.dart'; import 'package:tone_snap/generated/assets.dart'; import 'package:tone_snap/modules/sidea/controllers/player_controller.dart'; import 'package:tone_snap/modules/sidea/initial/initial_controller.dart'; @@ -46,7 +48,9 @@ class ChangeVoiceController extends GetxController { @override void onInit() { super.onInit(); - InterstitialAdManager().loadAd(); + if (Platform.isIOS) { + InterstitialAdManager().loadAd(); + } filePath = Get.arguments; // playerController.setFilePath(filePath); @@ -104,62 +108,121 @@ class ChangeVoiceController extends GetxController { /// 保存 Future save() async { - // 显示插页广告 - InterstitialAdManager().showAdIfAvailable(); + if (Platform.isIOS) { + // 显示插页广告 + InterstitialAdManager().showAdIfAvailable( + onTap: () async { + // 停止播放 + if (playerController.isPlaying.value) playerController.stopPlay(); + BaseEasyLoading.loading(); - // 停止播放 - if (playerController.isPlaying.value) playerController.stopPlay(); - BaseEasyLoading.loading(); + try { + // 若是assets路径,转换为文件路径 + if (filePath.contains('assets')) { + filePath = await FileUtil.getAssetsToFilePath(filePath); + } - try { - // 若是assets路径,转换为文件路径 - if (filePath.contains('assets')) { - filePath = await FileUtil.getAssetsToFilePath(filePath); - } + // 输出目录 + final outputDir = await LocalPathUtil.getVoiceChangeOutputDir(); + String fileName = '${DateUtil.getNowTimeStr().replaceAll(' ', '_')}_output.mp3'; + String outputPath = '${outputDir.path}/$fileName'; - // 输出目录 - final outputDir = await LocalPathUtil.getVoiceChangeOutputDir(); - String fileName = '${DateUtil.getNowTimeStr().replaceAll(' ', '_')}_output.mp3'; - String outputPath = '${outputDir.path}/$fileName'; + var filter = ""; + // var timber = timberList.firstWhereOrNull((e) => e.check); + // if (timber != null) { + // int index = timberList.indexOf(timber); + // if (index == 5) filter = ",afftdn=nf=-30"; + // } - var filter = ""; - // var timber = timberList.firstWhereOrNull((e) => e.check); - // if (timber != null) { - // int index = timberList.indexOf(timber); - // if (index == 5) filter = ",afftdn=nf=-30"; - // } + if (currentIndex.value == 5) filter = ",afftdn=nf=-30,aecho=0.8:0.88:60:0.4"; - if (currentIndex.value == 5) filter = ",afftdn=nf=-30,aecho=0.8:0.88:60:0.4"; + // 获取原始采样率 + String sampleRate = await _getSampleRate() ?? '24000'; - // 获取原始采样率 - String sampleRate = await _getSampleRate() ?? '24000'; + // 构建 FFmpeg 命令 + final String command = '-i $filePath -af "asetrate=$sampleRate*${toneValue.value},atempo=${soundSpeedValue.value}$filter" $outputPath'; - // 构建 FFmpeg 命令 - final String command = '-i $filePath -af "asetrate=$sampleRate*${toneValue.value},atempo=${soundSpeedValue.value}$filter" $outputPath'; + // 执行 FFmpeg 命令 + FFmpegSession session = await FFmpegKit.execute(command); - // 执行 FFmpeg 命令 - FFmpegSession session = await FFmpegKit.execute(command); + // 获取执行结果 + final returnCode = await session.getReturnCode(); + if (ReturnCode.isSuccess(returnCode)) { + LogUtil.d('Audio processing successful'); + try { + await MyVoiceBox().addData(VoiceModel(name: fileName, path: outputPath)); + BaseEasyLoading.toast('Save successful'); - // 获取执行结果 - final returnCode = await session.getReturnCode(); - if (ReturnCode.isSuccess(returnCode)) { - LogUtil.d('Audio processing successful'); - try { - await MyVoiceData().addData(VoiceModel(name: fileName, path: outputPath)); - BaseEasyLoading.toast('Save successful'); + // 回到首页-我的页面 + Get.until((route) => route.settings.name == AppRoutes.initialA); + InitialController.to.onBottomAppBarItemChanged(2); + } catch (e) { + BaseEasyLoading.toast('Save failed'); + } + } else { + LogUtil.d('Audio processing failed'); + BaseEasyLoading.toast('Audio processing failed'); + } + } catch (e) { + BaseEasyLoading.toast('Audio processing failed'); + } + }, + ); + } else { + // 停止播放 + if (playerController.isPlaying.value) playerController.stopPlay(); + BaseEasyLoading.loading(); - // 回到首页-我的页面 - Get.until((route) => route.settings.name == AppRoutes.initialA); - InitialController.to.onBottomAppBarItemChanged(2); - } catch (e) { - BaseEasyLoading.toast('Save failed'); + try { + // 若是assets路径,转换为文件路径 + if (filePath.contains('assets')) { + filePath = await FileUtil.getAssetsToFilePath(filePath); } - } else { - LogUtil.d('Audio processing failed'); + + // 输出目录 + final outputDir = await LocalPathUtil.getVoiceChangeOutputDir(); + String fileName = '${DateUtil.getNowTimeStr().replaceAll(' ', '_')}_output.mp3'; + String outputPath = '${outputDir.path}/$fileName'; + + var filter = ""; + // var timber = timberList.firstWhereOrNull((e) => e.check); + // if (timber != null) { + // int index = timberList.indexOf(timber); + // if (index == 5) filter = ",afftdn=nf=-30"; + // } + + if (currentIndex.value == 5) filter = ",afftdn=nf=-30,aecho=0.8:0.88:60:0.4"; + + // 获取原始采样率 + String sampleRate = await _getSampleRate() ?? '24000'; + + // 构建 FFmpeg 命令 + final String command = '-i $filePath -af "asetrate=$sampleRate*${toneValue.value},atempo=${soundSpeedValue.value}$filter" $outputPath'; + + // 执行 FFmpeg 命令 + FFmpegSession session = await FFmpegKit.execute(command); + + // 获取执行结果 + final returnCode = await session.getReturnCode(); + if (ReturnCode.isSuccess(returnCode)) { + LogUtil.d('Audio processing successful'); + try { + await MyVoiceBox().addData(VoiceModel(name: fileName, path: outputPath)); + BaseEasyLoading.toast('Save successful'); + + // 回到首页-我的页面 + Get.until((route) => route.settings.name == AppRoutes.initialA); + InitialController.to.onBottomAppBarItemChanged(2); + } catch (e) { + BaseEasyLoading.toast('Save failed'); + } + } else { + LogUtil.d('Audio processing failed'); + BaseEasyLoading.toast('Audio processing failed'); + } + } catch (e) { BaseEasyLoading.toast('Audio processing failed'); } - } catch (e) { - BaseEasyLoading.toast('Audio processing failed'); } } diff --git a/lib/modules/sidea/favourite/favourite_controller.dart b/lib/modules/sidea/favourite/favourite_controller.dart index 411f2ff..3053a68 100644 --- a/lib/modules/sidea/favourite/favourite_controller.dart +++ b/lib/modules/sidea/favourite/favourite_controller.dart @@ -4,8 +4,8 @@ import 'package:tone_snap/components/base_easyloading.dart'; import 'package:tone_snap/components/dialog/remind_dialog.dart'; import 'package:tone_snap/components/dialog/rename_dialog.dart'; import 'package:tone_snap/components/view_state_widget.dart'; -import 'package:tone_snap/data/sidea/models/voice_model.dart'; -import 'package:tone_snap/data/sidea/storage/favorite_data.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/data/storage/favorite_box.dart'; import 'package:tone_snap/modules/sidea/initial/initial_controller.dart'; import 'package:tone_snap/routes/app_routes.dart'; @@ -22,7 +22,7 @@ class FavouriteController extends GetxController { } void getData() { - voiceList.value = FavoriteData().getList().reversed.toList(); + voiceList.value = FavoriteBox().getList().reversed.toList(); _refreshList(); } diff --git a/lib/modules/sidea/home/home_controller.dart b/lib/modules/sidea/home/home_controller.dart index 2804102..4dc7258 100644 --- a/lib/modules/sidea/home/home_controller.dart +++ b/lib/modules/sidea/home/home_controller.dart @@ -1,6 +1,6 @@ import 'package:get/get.dart'; import 'package:tone_snap/components/base_easyloading.dart'; -import 'package:tone_snap/data/sidea/models/voice_model.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; import 'package:tone_snap/generated/assets.dart'; import 'package:tone_snap/modules/sidea/controllers/player_controller.dart'; import 'package:tone_snap/modules/sidea/initial/initial_controller.dart'; diff --git a/lib/modules/sidea/home/home_view.dart b/lib/modules/sidea/home/home_view.dart index ce20ddb..2d5d724 100644 --- a/lib/modules/sidea/home/home_view.dart +++ b/lib/modules/sidea/home/home_view.dart @@ -4,7 +4,7 @@ import 'package:get/get.dart'; import 'package:tone_snap/components/circular_notch_clipper.dart'; import 'package:tone_snap/components/my_marquee_text.dart'; import 'package:tone_snap/modules/sidea/widgets/head_label.dart'; -import 'package:tone_snap/data/sidea/models/voice_model.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; import 'package:tone_snap/generated/assets.dart'; import 'package:tone_snap/modules/sidea/home/home_controller.dart'; import 'package:tone_snap/utils/obj_util.dart'; diff --git a/lib/modules/sidea/initial/initial_controller.dart b/lib/modules/sidea/initial/initial_controller.dart index c4935c2..65722a8 100644 --- a/lib/modules/sidea/initial/initial_controller.dart +++ b/lib/modules/sidea/initial/initial_controller.dart @@ -1,11 +1,12 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:tone_snap/global/app_lifecycle_reactor.dart'; -import 'package:tone_snap/ads/app_open_ad_manager.dart'; -import 'package:tone_snap/data/sidea/models/voice_model.dart'; -import 'package:tone_snap/data/sidea/storage/favorite_data.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/data/storage/favorite_box.dart'; import 'package:tone_snap/firebase/firebase_analytics_manager.dart'; import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/global/app_lifecycle_reactor.dart'; import 'package:tone_snap/modules/sidea/controllers/player_controller.dart'; import 'package:tone_snap/modules/sidea/favourite/favourite_controller.dart'; import 'package:tone_snap/modules/sidea/home/home_view.dart'; @@ -14,8 +15,6 @@ import 'package:tone_snap/modules/sidea/my_voice/my_voice_controller.dart'; import 'package:tone_snap/modules/sidea/settings/settings_view.dart'; import 'package:tone_snap/routes/app_routes.dart'; -import '../../../utils/tracking_authorization_util.dart'; - class InitialController extends GetxController { static InitialController get to => Get.find(); var playerController = PlayerController.to; @@ -37,11 +36,8 @@ class InitialController extends GetxController { @override void onInit() { super.onInit(); - // ATT授权 - TrackingAuthorizationUtil.requestTrackingAuthorization(); - - _appLifecycleReactor = AppLifecycleReactor(); - _appLifecycleReactor.listenToAppStateChanges(); + // _appLifecycleReactor = AppLifecycleReactor(); + // _appLifecycleReactor.listenToAppStateChanges(); pageController = PageController(initialPage: currentIndex.value); } @@ -49,8 +45,8 @@ class InitialController extends GetxController { @override void onReady() async { super.onReady(); - await FirebaseAnalyticsManager.setCrashlyticsCollectionEnabled(); - _addEventLog(); + // await FirebaseAnalyticsManager.setCrashlyticsCollectionEnabled(); + // _addEventLog(); } @override @@ -64,9 +60,9 @@ class InitialController extends GetxController { await PlayerController.to.stopPlay(); Get.toNamed(AppRoutes.uploadMethod); } else { - if (index == 0) { - _addEventLog(); - } + // if (index == 0) { + // _addEventLog(); + // } if (index == 2) _refreshMe(); currentIndex.value = index; pageController.jumpToPage(index); @@ -84,7 +80,7 @@ class InitialController extends GetxController { } VoiceModel? getIsFavouriteModel() { - final list = FavoriteData().getList(); + final list = FavoriteBox().getList(); return list.firstWhereOrNull((e) => e.path == currentPlayVoiceModel.value?.path); } @@ -94,7 +90,7 @@ class InitialController extends GetxController { isFavourite.value = false; } else { if (currentPlayVoiceModel.value != null) { - await FavoriteData().addData(currentPlayVoiceModel.value!.copyWith()); + await FavoriteBox().addData(currentPlayVoiceModel.value!.copyWith()); isFavourite.value = true; } } diff --git a/lib/modules/sidea/me/me_controller.dart b/lib/modules/sidea/me/me_controller.dart index d8467ab..1883f9b 100644 --- a/lib/modules/sidea/me/me_controller.dart +++ b/lib/modules/sidea/me/me_controller.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:tone_snap/modules/sidea/favourite/favourite_view.dart'; import 'package:tone_snap/modules/sidea/my_voice/my_voice_view.dart'; -class MeController extends GetxController with GetTickerProviderStateMixin { +class MeController extends GetxController with GetSingleTickerProviderStateMixin { late TabController tabController; final labels = ['My Voice', 'Favourite']; final pages = [const MyVoiceView(), const FavouriteView()]; diff --git a/lib/modules/sidea/my_voice/my_voice_controller.dart b/lib/modules/sidea/my_voice/my_voice_controller.dart index 9e25475..1e7660f 100644 --- a/lib/modules/sidea/my_voice/my_voice_controller.dart +++ b/lib/modules/sidea/my_voice/my_voice_controller.dart @@ -4,8 +4,8 @@ import 'package:tone_snap/components/base_easyloading.dart'; import 'package:tone_snap/components/dialog/remind_dialog.dart'; import 'package:tone_snap/components/dialog/rename_dialog.dart'; import 'package:tone_snap/components/view_state_widget.dart'; -import 'package:tone_snap/data/sidea/models/voice_model.dart'; -import 'package:tone_snap/data/sidea/storage/my_voice_data.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/data/storage/my_voice_box.dart'; import 'package:tone_snap/modules/sidea/initial/initial_controller.dart'; import 'package:tone_snap/routes/app_routes.dart'; @@ -22,7 +22,7 @@ class MyVoiceController extends GetxController { } void getData() { - voiceList.value = MyVoiceData().getList().reversed.toList(); + voiceList.value = MyVoiceBox().getList().reversed.toList(); _refreshList(); } diff --git a/lib/modules/sidea/play_sound/play_sound_controller.dart b/lib/modules/sidea/play_sound/play_sound_controller.dart index 7c7f631..351b9ff 100644 --- a/lib/modules/sidea/play_sound/play_sound_controller.dart +++ b/lib/modules/sidea/play_sound/play_sound_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:get/get.dart'; -import 'package:tone_snap/data/sidea/models/voice_model.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; import 'package:tone_snap/modules/sidea/controllers/player_controller.dart'; import 'package:tone_snap/modules/sidea/initial/initial_controller.dart'; import 'package:tone_snap/routes/app_routes.dart'; diff --git a/lib/modules/sidea/splash/splash_controller.dart b/lib/modules/sidea/splash/splash_controller.dart deleted file mode 100644 index 6a91131..0000000 --- a/lib/modules/sidea/splash/splash_controller.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:tone_snap/ads/app_open_ad_manager.dart'; -import 'package:tone_snap/utils/tracking_authorization_util.dart'; - -class SplashController extends GetxController with GetSingleTickerProviderStateMixin { - var processValue = 0.0.obs; - late AnimationController _controller; - late Animation _animation; - - @override - void onInit() { - super.onInit(); - _controller = AnimationController( - duration: const Duration(seconds: 10), - vsync: this, - ); - - // 创建 Tween 并绑定到 AnimationController - _animation = Tween(begin: 0, end: 1).animate(_controller) - ..addListener(() { - processValue.value = _animation.value; - if (processValue.value >= 1) { - // 显示开屏广告 - AppOpenAdManager().showAdIfAvailable(); - } - }); - - // 启动动画 - _controller.forward(); - - // 延迟3秒执行 - Future.delayed(const Duration(seconds: 3), () { - // ATT授权 - TrackingAuthorizationUtil.requestTrackingAuthorization(); - }); - } - - @override - void onClose() { - _controller.dispose(); - super.onClose(); - } -} diff --git a/lib/modules/sidea/widgets/my_voice_item.dart b/lib/modules/sidea/widgets/my_voice_item.dart index b399adf..ce6d863 100644 --- a/lib/modules/sidea/widgets/my_voice_item.dart +++ b/lib/modules/sidea/widgets/my_voice_item.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:tone_snap/components/my_marquee_text.dart'; -import 'package:tone_snap/data/sidea/models/voice_model.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; import 'package:tone_snap/generated/assets.dart'; import 'package:tone_snap/utils/obj_util.dart'; diff --git a/lib/modules/sideb/album/album_binding.dart b/lib/modules/sideb/album/album_binding.dart new file mode 100644 index 0000000..06be2c0 --- /dev/null +++ b/lib/modules/sideb/album/album_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/sideb/album/album_controller.dart'; + +class AlbumBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => AlbumController()); + } +} diff --git a/lib/modules/sideb/album/album_controller.dart b/lib/modules/sideb/album/album_controller.dart new file mode 100644 index 0000000..b8295cb --- /dev/null +++ b/lib/modules/sideb/album/album_controller.dart @@ -0,0 +1,104 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/data/api/music_api.dart'; +import 'package:tone_snap/data/models/browse_album_model.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart'; +import 'package:tone_snap/routes/app_routes.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class AlbumController extends GetxController { + var musicPlayerController = MusicPlayerController.to; + String? browseId; + String? params; + + /// 背景图、标题 、描述 + var bgThumbnail = Rx(null); + var title = Rx(null); + var description = Rx(null); + + /// 专辑列表 + var albumList = [].obs; + + @override + void onInit() { + super.onInit(); + browseId = Get.arguments['browseId']; + params = Get.arguments['params']; + } + + @override + void onReady() { + super.onReady(); + _requestAlbum(); + } + + /// 请求专辑 + Future _requestAlbum() async { + Map queryParameters = { + 'browseId': browseId, + 'params': params, + 'prettyPrint': false + }; + BrowseAlbumModel? browseAlbumModel = await MusicApi.browse(queryParameters: queryParameters, formJson: BrowseAlbumModel.fromMap); + if (browseAlbumModel != null) { + var thumbnails = browseAlbumModel.background?.musicThumbnailRenderer?.thumbnail?.thumbnails; + if (thumbnails != null && thumbnails.isNotEmpty) { + bgThumbnail.value = thumbnails.last.url; + } + var tabs = browseAlbumModel.contents?.twoColumnBrowseResultsRenderer?.tabs; + if (tabs != null && tabs.isNotEmpty) { + var contents = tabs[0].tabRenderer?.content?.sectionListRenderer?.contents; + if (contents != null && contents.isNotEmpty) { + var runs = contents[0].musicResponsiveHeaderRenderer?.title?.runs; + if (runs != null && runs.isNotEmpty) { + title.value = runs[0].text; + } + var runs1 = contents[0].musicResponsiveHeaderRenderer?.description?.musicDescriptionShelfRenderer?.description?.runs; + if (runs1 != null && runs1.isNotEmpty) { + description.value = runs1.map((e) => e.text).join(); + } + } + } + + var contents = browseAlbumModel.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer?.contents; + if (contents != null && contents.isNotEmpty) { + var contents1 = contents[0].musicShelfRenderer?.contents; + if (contents1 != null && contents1.isNotEmpty) { + for (var o in contents1) { + var playListModel = MusicModel(); + var flexColumns = o.musicResponsiveListItemRenderer?.flexColumns; + if (flexColumns != null && flexColumns.isNotEmpty) { + var runs = flexColumns[0].musicResponsiveListItemFlexColumnRenderer?.text?.runs; + if (runs != null && runs.isNotEmpty) { + playListModel.title = runs[0].text; + } + for (var o in flexColumns) { + var runs = o.musicResponsiveListItemFlexColumnRenderer?.text?.runs; + if (runs != null && runs.isNotEmpty) { + playListModel.subTitle = runs.map((e) => e.text).join(); + } + } + } + var fixedColumns = o.musicResponsiveListItemRenderer?.fixedColumns; + if (fixedColumns != null && fixedColumns.isNotEmpty) { + var runs = fixedColumns[0].musicResponsiveListItemFixedColumnRenderer?.text?.runs; + if (runs != null && runs.isNotEmpty) { + playListModel.subTitle = '${ObjUtil.getStr(playListModel.subTitle)} • ${runs.map((e) => e.text).join()}'; + } + } + var playlistItemData = o.musicResponsiveListItemRenderer?.playlistItemData; + if (playlistItemData != null) { + playListModel.videoId = playlistItemData.videoId; + } + albumList.add(playListModel); + } + } + } + } + } + + /// 点击专辑列表歌曲 + void onTapAlbumItem(int index, MusicModel model) { + Get.toNamed(AppRoutes.playPage, arguments: {'playList': albumList, 'videoId': model.videoId}); + } +} diff --git a/lib/modules/sideb/album/album_view.dart b/lib/modules/sideb/album/album_view.dart new file mode 100644 index 0000000..7c254a8 --- /dev/null +++ b/lib/modules/sideb/album/album_view.dart @@ -0,0 +1,229 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_scrollbar.dart'; +import 'package:tone_snap/components/music_bar.dart'; +import 'package:tone_snap/components/my_marquee_text.dart'; +import 'package:tone_snap/components/network_image_widget.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/sideb/album/album_controller.dart'; +import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart'; +import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; +import 'package:tone_snap/res/themes/app_sizes.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class AlbumView extends StatelessWidget { + AlbumView({super.key}); + + final controller = Get.find(); + final musicPlayerController = MusicPlayerController.to; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Obx(() { + return NetworkImageWidget( + url: controller.bgThumbnail.value, + width: 1.sw, + height: 413.h, + noPlaceholder: true, + ); + }), + Scaffold( + backgroundColor: Colors.transparent, + body: Column( + children: [ + const MusicAppbar(), + SizedBox(height: 152.h), + _buildIntroduction(), + _buildList(), + Obx(() { + return Visibility( + visible: MusicBar().isShow.value, + child: SizedBox(height: paddingBottomMusicBarHeight(context)), + ); + }), + ], + ), + ), + ], + ); + } + + Widget _buildIntroduction() { + return Stack( + children: [ + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: ClipRRect( + borderRadius: BorderRadius.only(topLeft: Radius.circular(16.r), topRight: Radius.circular(16.r)), + child: Image.asset( + Assets.sideBAlbumTitleBg, + width: 1.sw, + height: 173.h, + fit: BoxFit.fitWidth, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18).w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 18.h), + Obx(() { + return Text( + ObjUtil.getStr(controller.title.value), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 22.sp, + fontWeight: FontWeight.w500, + ), + ); + }), + SizedBox(height: 12.h), + Obx(() { + return Text( + ObjUtil.getStr(controller.description.value), + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0x99FFFFFF), + fontSize: 12.sp, + ), + ); + }), + SizedBox(height: 24.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 138.w, + height: 32.h, + padding: const EdgeInsets.symmetric(horizontal: 4).w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16).r, + color: const Color(0x1A80F988), + ), + child: Row( + children: [ + Image.asset( + Assets.sideBAlbumTotal, + width: 24.w, + height: 24.w, + ), + SizedBox(width: 4.w), + Expanded( + child: Obx(() { + return Text( + 'Play all (${controller.albumList.length})', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ); + }), + ), + ], + ), + ), + Image.asset( + Assets.sideBNotCollectionAlbum, + width: 24.w, + height: 24.w, + ), + ], + ), + ], + ), + ), + ], + ); + } + + Widget _buildList() { + return Expanded( + child: Container( + color: const Color(0xFF121212), + child: BaseScrollbar( + child: Obx(() { + return ListView.builder( + itemCount: controller.albumList.length, + itemBuilder: (context, index) { + return _buildListItem(index, controller.albumList[index]); + }, + ); + }), + ), + ), + ); + } + + Widget _buildListItem(int index, MusicModel model) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => controller.onTapAlbumItem(index, model), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8).h, + child: Row( + children: [ + SizedBox(width: 18.w), + Text( + (index + 1).toString(), + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + return MyMarqueeText( + enable: musicPlayerController.musicModel.value.videoId == model.videoId, + text: ObjUtil.getStr(model.title), + textStyle: TextStyle( + color: musicPlayerController.musicModel.value.videoId == model.videoId + ? seedColor + : Colors.white, + fontSize: 14.sp, + ), + ); + }), + SizedBox(height: 8.h), + Obx(() { + return MyMarqueeText( + enable: musicPlayerController.musicModel.value.videoId == model.videoId, + text: ObjUtil.getStr(model.subTitle), + textStyle: TextStyle( + color: musicPlayerController.musicModel.value.videoId == model.videoId + ? seedColor + : const Color(0x99FFFFFF), + fontSize: 12.sp, + ), + ); + }), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/sideb/collect_playlists/collect_playlists_binding.dart b/lib/modules/sideb/collect_playlists/collect_playlists_binding.dart new file mode 100644 index 0000000..a0ccb9d --- /dev/null +++ b/lib/modules/sideb/collect_playlists/collect_playlists_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'collect_playlists_controller.dart'; + +class CollectPlaylistsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => CollectPlaylistsController()); + } +} diff --git a/lib/modules/sideb/collect_playlists/collect_playlists_controller.dart b/lib/modules/sideb/collect_playlists/collect_playlists_controller.dart new file mode 100644 index 0000000..0c7026f --- /dev/null +++ b/lib/modules/sideb/collect_playlists/collect_playlists_controller.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class CollectPlaylistsController extends GetxController { + ScrollController scrollController = ScrollController(); + + @override + void onClose() { + scrollController.dispose(); + super.onClose(); + } +} diff --git a/lib/modules/sideb/collect_playlists/collect_playlists_view.dart b/lib/modules/sideb/collect_playlists/collect_playlists_view.dart new file mode 100644 index 0000000..5b96848 --- /dev/null +++ b/lib/modules/sideb/collect_playlists/collect_playlists_view.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_scrollbar.dart'; +import 'package:tone_snap/components/network_image_widget.dart'; + +import 'collect_playlists_controller.dart'; + +class CollectPlaylistsView extends GetView { + const CollectPlaylistsView({super.key}); + + @override + Widget build(BuildContext context) { + Get.find(); + + return BaseScrollbar( + scrollController: controller.scrollController, + child: ListView.builder( + controller: controller.scrollController, + itemCount: 10, + itemBuilder: (context, index) { + return _buildItem(); + }, + ), + ); + } + + Widget _buildItem() { + return InkWell( + onTap: (){}, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 16.w), + child: Row( + children: [ + NetworkImageWidget( + url: '', + width: 60.w, + height: 60.w, + radius: 8.r, + ), + SizedBox(width: 14.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'There For You', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + ), + ), + SizedBox(height: 4.h), + Text( + '234 songs', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0xFF999999), + fontSize: 12.sp, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/sideb/controllers/music_player_controller.dart b/lib/modules/sideb/controllers/music_player_controller.dart index d789359..fd64113 100644 --- a/lib/modules/sideb/controllers/music_player_controller.dart +++ b/lib/modules/sideb/controllers/music_player_controller.dart @@ -3,16 +3,22 @@ // Description: 音乐播放器控制器 import 'dart:async'; -import 'dart:io'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/components/music_bar.dart'; +import 'package:tone_snap/data/api/music_api.dart'; import 'package:tone_snap/data/cache/music_cache_manager.dart'; +import 'package:tone_snap/data/enum/play_mode.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/models/player_model.dart'; +import 'package:tone_snap/data/storage/music_box.dart'; +import 'package:tone_snap/routes/app_routes.dart'; import 'package:tone_snap/utils/audio_util.dart'; -import 'package:tone_snap/utils/date_util.dart'; import 'package:tone_snap/utils/log_util.dart'; +import 'package:tone_snap/utils/num_util.dart'; class MusicPlayerController extends GetxController { static MusicPlayerController get to => Get.put(MusicPlayerController(), permanent: true); @@ -22,31 +28,46 @@ class MusicPlayerController extends GetxController { StreamSubscription? _positionSubscription; StreamSubscription? _playerStateSubscription; - /// 缓存管理器 - // final cacheManager = DefaultCacheManager(); - final cacheManager = MusicCacheManager.instance; - var cacheKey = ''; - - /// 是否正在从缓存播放中 - var _isPlayingFromCache = false; - - /// 当前播放的url - var currentUrl = ''; - - /// 是否正在播放 - var isPlaying = false.obs; - /// 总时长、已播放的时长、缓冲时长 var totalDuration = Duration.zero.obs; var positionDuration = Duration.zero.obs; var bufferedDuration = Duration.zero.obs; - /// 记录进度条拖动前缓冲的位置 - Duration? bufferedPosition; + /// 缓存管理器 + // final _cacheManager = DefaultCacheManager(); + final _cacheManager = MusicCacheManager.instance; - /// 拖动前是否是播放状态 + /// 是否正在播放 + var isPlaying = false.obs; + + /// 播放器处理状态 + var processingState = ProcessingState.idle.obs; + + /// _player.seek(Duration.zero) 时,这可能会导致播放状态再次变为 completed。 + /// 为了避免这种情况,在处理 completed 状态时添加一个标志位,确保在处理完成状态时不会重复触发 + /// 是否已经处理过 completed 状态 + bool _isCompletedHandled = false; + + /// 拖动开始时是否是播放状态 var seekFrontPlaying = true; + /// 播放模式,默认列表循环 + var playMode = MusicBox().getPlayMode().obs; + var _playModeIndex = 0; + + /// 播放列表 + var playList = [].obs; + + /// 播放历史 videoId 集合 + /// 随机播放切换到上一首时,从历史记录中取出上一个播放的 videoId + final List _playHistory = []; + + /// 播放的歌曲模型 + var musicModel = MusicModel().obs; + + /// 当前播放的歌曲索引 + int _currentIndex = 0; + @override void onInit() { super.onInit(); @@ -55,118 +76,96 @@ class MusicPlayerController extends GetxController { @override void onClose() { + MusicBar().hide(); _cancelListening(); _player.dispose(); super.onClose(); } - /// 播放新的 url - Future playNewUrl(String newUrl, String videoId) async { - await _initializePlayer(newUrl, videoId); - _player.play(); + /// 播放指定索引的歌曲 + void playMusicSpecifyIndex(int index) { + _currentIndex = index; + playMusic(); + } - // 使用的 music_$videoId 作为唯一的 key - cacheKey = 'music_$videoId'; + /// 设置播放列表 + void setPlayList(String videoId, List list) { + _resetPlaybackStatus(); + playList.value = list; + _playHistory.clear(); + + MusicModel? musicModel = playList.firstWhereOrNull((e) => e.videoId == videoId); + int index = musicModel != null ? playList.indexOf(musicModel) : 0; + _currentIndex = index; + } + + /// 播放歌曲 + Future playMusic() async { + Get.currentRoute != AppRoutes.playPage ? MusicBar().show() : MusicBar().hide(); + _resetPlaybackStatus(); + if (playList.isNotEmpty) { + musicModel.update((e) { + e?.thumbnail = playList[_currentIndex].thumbnail; + e?.title = playList[_currentIndex].title; + e?.subTitle = playList[_currentIndex].subTitle; + }); + if (playList[_currentIndex].url == null) { + await _getMusicUrl(); + } + if (playList[_currentIndex].url == null) { + BaseEasyLoading.toast('Resource not obtained'); + // nextTrack(); + return; + } + musicModel.value = playList[_currentIndex].copyWith(); + await _initializePlayer(); + } + } + + /// 获取当前歌曲的播放 url + Future _getMusicUrl() async { + PlayerModel? model = await MusicApi.player(videoId: playList[_currentIndex].videoId); + if (model != null && model.streamingData != null) { + var formats = model.streamingData?.formats; + if (formats != null && playList.isNotEmpty) { + playList[_currentIndex].url = formats[0].url; + } + } + if (model != null && model.videoDetails?.thumbnail?.thumbnails != null) { + + } } /// 设置 url 和监听器 - Future _initializePlayer(String url, String videoId) async { + Future _initializePlayer() async { try { - currentUrl = url; // 检查是否有缓存 - FileInfo? fileInfo = await cacheManager.getFileFromMemory(cacheKey); + FileInfo? fileInfo = await MusicCacheManager.checkCache(musicModel.value.videoId!); if (fileInfo != null) { - LogUtil.d('有缓存,文件路径=${fileInfo.file.path}'); + LogUtil.d('读取缓存=${fileInfo.file.path}'); + // 如果有缓存,使用缓存文件 await _player.setFilePath(fileInfo.file.path); - _isPlayingFromCache = true; } else { - // 如果没有缓存,通过 url 加载音源并开始播放,同时启动缓存下载 - await _player.setUrl(currentUrl); - _isPlayingFromCache = false; + // 如果没有缓存,通过 url 加载音源并开始播放, + await _player.setUrl(musicModel.value.url!); - // 下载并缓存文件 - cacheManager.downloadFile(currentUrl, key: cacheKey).then((fileInfo) { - // 如果缓存下载完成且当前不是从缓存播放,切换到缓存文件 - if (!_isPlayingFromCache) { - LogUtil.d('新歌曲缓存路径=${fileInfo.file.path}'); - _player.setFilePath(fileInfo.file.path); - _isPlayingFromCache = true; - } + // 同时启动缓存下载 + _cacheManager.downloadFile(musicModel.value.url!, key: MusicCacheManager.getCacheKey(musicModel.value.videoId!)).then((fileInfo) { + LogUtil.d('缓存下载成功=${fileInfo.file.path}'); }); } - _player.setLoopMode(LoopMode.off); _cancelListening(); _addListening(); + _player.play(); } catch (e) { LogUtil.e('Error initializing player: $e'); BaseEasyLoading.toast('Error initializing player'); } } - /// 播放/暂停 - Future playPause() async { - if (_player.playing) { - _player.pause(); - } else { - if (_player.processingState == ProcessingState.idle) { - BaseEasyLoading.loading(); - await _player.load(); - BaseEasyLoading.dismiss(); - } - _player.play(); - } - } - - /// 停止播放 - void stopPlay() { - _player.stop(); - _player.seek(Duration.zero); - positionDuration.value = Duration.zero; - } - - /// 将播放器的播放位置设置为指定的时间 - void seekToPosition(double value) { - if (_player.processingState == ProcessingState.ready) { - positionDuration.value = Duration(seconds: value.toInt()); - _player.seek(positionDuration.value); - } - } - - /// 开始/结束拖动进度条 - Future seekStartEnd(int value) async { - bufferedPosition = _player.bufferedPosition; - if (_player.processingState == ProcessingState.ready) { - if (value == 0) { - seekFrontPlaying = _player.playing; - if (_player.playing) { - _player.pause(); - } - } else { - // 拖动前如果是播放状态,拖动完成后恢复播放,否则相反 - if (seekFrontPlaying) { - _player.play(); - } - } - } - } - - /// 上一首 - void seekToPrevious() { - _player.seekToPrevious(); - } - - /// 下一首 - void seekToNext() { - _player.seekToNext(); - } - /// 监听 void _addListening() { - _bufferedSubscription = _player.bufferedPositionStream.listen((bufferedPosition) { - bufferedDuration.value = bufferedPosition; - }); - _durationSubscription = _player.durationStream.listen((duration) { totalDuration.value = duration ?? Duration.zero; }); @@ -175,8 +174,18 @@ class MusicPlayerController extends GetxController { positionDuration.value = position; }); + _bufferedSubscription = _player.bufferedPositionStream.listen((bufferedPosition) { + int comparison = bufferedPosition.compareTo(totalDuration.value); + if (comparison > 0) { + bufferedDuration.value = totalDuration.value; + } else { + bufferedDuration.value = bufferedPosition; + } + }); + _playerStateSubscription = _player.playerStateStream.listen((playerState) { isPlaying.value = _player.playing; + processingState.value = playerState.processingState; switch (playerState.processingState) { case ProcessingState.idle: break; @@ -185,9 +194,14 @@ class MusicPlayerController extends GetxController { case ProcessingState.buffering: break; case ProcessingState.ready: + _isCompletedHandled = false; break; case ProcessingState.completed: - stopPlay(); + if (!_isCompletedHandled) { + _isCompletedHandled = true; + _player.seek(Duration.zero); + nextTrack(); + } break; } }); @@ -201,23 +215,118 @@ class MusicPlayerController extends GetxController { _playerStateSubscription?.cancel(); } - /// 返回格式化后的总时长 - String getTotalDuration() { - return DateUtil.playDuration(totalDuration.value); - } - - /// 返回格式化后的已持续时间 - String getPositionDuration() { - return DateUtil.playDuration(positionDuration.value); - } - - /// 文件是否存在 - Future _fileExists(String path) async { - if (await File(path).exists()) { - return true; - } else { - BaseEasyLoading.toast('Local resource does not exist'); - return false; + /// 将播放器的播放位置设置为指定的时间 + void seekToPosition(double value) { + if (processingState.value == ProcessingState.ready) { + positionDuration.value = Duration(seconds: value.toInt()); + _player.seek(positionDuration.value); } } + + /// 开始/结束拖动进度条 + Future seekStartEnd(int value) async { + if (processingState.value == ProcessingState.ready) { + if (value == 0) { + seekFrontPlaying = _player.playing; + if (_player.playing) { + _player.pause(); + } + } else { + // 拖动前如果是播放状态,拖动完成后恢复播放 + if (seekFrontPlaying) { + _player.play(); + } + } + } + } + + /// 播放/暂停 + Future playPause() async { + if (_player.playing) { + _player.pause(); + } else { + if (processingState.value == ProcessingState.ready) { + _player.play(); + } + } + } + + /// 上一首 + void previousTrack() { + if (playList.isNotEmpty) { + switch(playMode.value) { + case PlayMode.listLoop: + _currentIndex = (_currentIndex - 1 + playList.length) % playList.length; + playMusic(); + break; + case PlayMode.random: + bool historyExist = false; + for (var i = _playHistory.length - 1; i >= 0; --i) { + var history = _playHistory[i]; + MusicModel? model = playList.firstWhereOrNull((e) => e.videoId == history); + if (model != null) { + _currentIndex = playList.indexOf(model); + _playHistory.remove(history); + historyExist = true; + break; + } else { + _playHistory.remove(history); + } + } + if (!historyExist) { + _getRandomNumber(); + } + playMusic(); + break; + case PlayMode.singleCycle: + break; + } + } + } + + /// 下一首 + void nextTrack() { + if (playList.isNotEmpty) { + switch(playMode.value) { + case PlayMode.listLoop: + _currentIndex = (_currentIndex + 1) % playList.length; + playMusic(); + break; + case PlayMode.random: + // 记录当前播放的索引 + _playHistory.add(musicModel.value.videoId!); + _getRandomNumber(); + playMusic(); + break; + case PlayMode.singleCycle: + break; + } + } + } + + /// 在列表范围内生成一个不包括当前索引的随机数 + void _getRandomNumber() { + _currentIndex = NumUtil.getRandomNumberExcludingCurrent(0, playList.length, _currentIndex); + } + + /// 切换播放模式 + void switchPlayMode() { + if (_playModeIndex == PlayMode.values.length - 1) { + _playModeIndex = 0; + } else { + _playModeIndex++; + } + playMode.value = PlayMode.values[_playModeIndex]; + MusicBox().putPlayMode(playMode.value); + } + + /// 重置播放状态 + void _resetPlaybackStatus() { + _player.stop(); + _player.seek(Duration.zero); + totalDuration.value = Duration.zero; + positionDuration.value = Duration.zero; + bufferedDuration.value = Duration.zero; + musicModel.update((model) => model = MusicModel()); + } } diff --git a/lib/modules/sideb/home/home_controller.dart b/lib/modules/sideb/home/home_controller.dart index 7c1872f..aaf39f0 100644 --- a/lib/modules/sideb/home/home_controller.dart +++ b/lib/modules/sideb/home/home_controller.dart @@ -1,13 +1,15 @@ import 'package:get/get.dart'; -import 'package:tone_snap/data/sideb/api/music_api.dart'; -import 'package:tone_snap/data/sideb/enum/browse_type.dart'; -import 'package:tone_snap/data/sideb/models/browse_model.dart'; -import 'package:tone_snap/data/sideb/models/home_model.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/api/music_api.dart'; +import 'package:tone_snap/data/enum/browse_type.dart'; +import 'package:tone_snap/data/models/browse_model.dart'; +import 'package:tone_snap/data/models/home_model.dart'; import 'package:tone_snap/routes/app_routes.dart'; import 'package:tone_snap/utils/obj_util.dart'; class HomeController extends GetxController { - var data = [].obs; + var viewState = ViewState.loading.obs; + var homeList = [].obs; String? visitorData; @override @@ -22,7 +24,7 @@ class HomeController extends GetxController { 'prettyPrint': false, 'browseId': 'FEmusic_home' }; - BrowseModel? browseModel = await MusicApi.browse(queryParameters: queryParameters); + BrowseModel? browseModel = await MusicApi.browse(queryParameters: queryParameters, formJson: BrowseModel.fromMap); if (browseModel != null) { _extractAssemblyData(browseModel); @@ -50,7 +52,7 @@ class HomeController extends GetxController { 'itct': clickTrackingParams, 'prettyPrint': false }; - BrowseModel? browseModel = await MusicApi.browse(queryParameters: queryParameters, visitorData: visitorData); + BrowseModel? browseModel = await MusicApi.browse(visitorData: visitorData, queryParameters: queryParameters, formJson: BrowseModel.fromMap); if (browseModel != null) { _extractAssemblyData(browseModel); @@ -109,6 +111,7 @@ class HomeController extends GetxController { } else if (runs[0].navigationEndpoint?.browseEndpoint != null) { model.browseType = runs[0].navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType; content.browseId = runs[0].navigationEndpoint?.browseEndpoint?.browseId; + content.params = runs[0].navigationEndpoint?.browseEndpoint?.params; } } } else { @@ -138,6 +141,7 @@ class HomeController extends GetxController { if (e2.musicTwoRowItemRenderer?.navigationEndpoint != null) { model.browseType = e2.musicTwoRowItemRenderer?.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType; content.browseId = e2.musicTwoRowItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId; + content.params = e2.musicTwoRowItemRenderer?.navigationEndpoint?.browseEndpoint?.params; } } if (thumbnails != null && thumbnails.isNotEmpty) { @@ -148,14 +152,20 @@ class HomeController extends GetxController { } // 根据类型判断是否添加 if (BrowseTypeExtension.isThereAny(model.browseType)) { - data.add(model); + homeList.add(model); } } } + viewState.value = homeList.isNotEmpty ? ViewState.normal : ViewState.empty; } - /// 去播放音乐 - void goPlayMusic(Content content) { - Get.toNamed(AppRoutes.playMusic, arguments: {'playlistId': content.playlistId, 'videoId': content.videoId}); + /// 打开播放页面 + void openPlayPage(Content content) { + Get.toNamed(AppRoutes.playPage, arguments: {'playlistId': content.playlistId, 'videoId': content.videoId}); + } + + /// 打开歌单/专辑页面 + void openSongSheet(Content content) { + Get.toNamed(AppRoutes.album, arguments: {'browseId': content.browseId}); } } diff --git a/lib/modules/sideb/home/home_view.dart b/lib/modules/sideb/home/home_view.dart index 03ad689..cc44a7c 100644 --- a/lib/modules/sideb/home/home_view.dart +++ b/lib/modules/sideb/home/home_view.dart @@ -2,11 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:tone_snap/components/base_scrollbar.dart'; -import 'package:tone_snap/data/sideb/enum/browse_type.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/enum/browse_type.dart'; +import 'package:tone_snap/data/models/home_model.dart'; import 'package:tone_snap/modules/sideb/widgets/album_item.dart'; import 'package:tone_snap/modules/sideb/widgets/atv_item.dart'; import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart'; -import 'package:tone_snap/data/sideb/models/home_model.dart'; import 'package:tone_snap/generated/assets.dart'; import 'package:tone_snap/modules/sideb/widgets/omv_item.dart'; import 'package:tone_snap/utils/obj_util.dart'; @@ -21,13 +22,13 @@ class HomeView extends GetView { Get.find(); return Column( children: [ - _buildTitleWidget(), + _buildTitle(), _buildListView(context), ], ); } - Widget _buildTitleWidget() { + Widget _buildTitle() { return MusicAppbar( title: 'Musicoo', showBackWidget: false, @@ -42,24 +43,74 @@ class HomeView extends GetView { Widget _buildListView(BuildContext context) { return Expanded( - child: BaseScrollbar( - child: Obx(() { - return ListView.separated( - itemCount: controller.data.length, - padding: EdgeInsets.fromLTRB(16.w, 0.h, 16.w, 16.h), - itemBuilder: (context, index) { - return _buildColumnItem(controller.data[index]); - }, - separatorBuilder: (context, index) { - return SizedBox(height: 16.h); - }, - ); - }), + child: Obx(() { + return ViewStateWidget( + viewState: controller.viewState.value, + child: BaseScrollbar( + child: ListView.separated( + itemCount: controller.homeList.length, + padding: EdgeInsets.fromLTRB(16.w, 0.h, 16.w, 16.h), + itemBuilder: (context, index) { + return _buildItem(controller.homeList[index]); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 16.h); + }, + ), + ), + ); + }), + ); + } + + Widget _buildItem(HomeModel model) { + if (!BrowseTypeExtension.isThereAny(model.browseType)) { + return Container(); + } + double? itemHeight; + if (model.browseType == BrowseType.musicVideoTypeAtv.name) { + itemHeight = 174.h; + } else if (model.browseType == BrowseType.musicPageTypeAlbum.name || + model.browseType == BrowseType.musicPageTypePlaylist.name) { + itemHeight = 195.h; + } else if (model.browseType == BrowseType.musicVideoTypeOmv.name) { + itemHeight = 277.h; + } + return SizedBox( + height: itemHeight, + child: Column( + children: [ + _labelWidget(ObjUtil.getStr(model.headerTitle)), + Expanded( + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: model.contents != null ? model.contents!.length : 0, + itemBuilder: (context, index) { + final content = model.contents![index]; + if (model.browseType == BrowseType.musicVideoTypeAtv.name) { + return AtvItem(content: content, + onTap: () => controller.openPlayPage(content)); + } else + if (model.browseType == BrowseType.musicPageTypeAlbum.name || + model.browseType == BrowseType.musicPageTypePlaylist.name) { + return AlbumItem(content: content, + onTap: () => controller.openSongSheet(content)); + } else + if (model.browseType == BrowseType.musicVideoTypeOmv.name) { + return OmvItem(content: content); + } + return Container(); + }, + separatorBuilder: (context, index) { + return SizedBox(width: 7.w); + }, + ), + ), + ], ), ); } - /// 模版标签 Widget _labelWidget(String label) { return InkWell( onTap: () {}, @@ -89,47 +140,4 @@ class HomeView extends GetView { ), ); } - - /// 垂直列表Item - Widget _buildColumnItem(HomeModel model) { - if (!BrowseTypeExtension.isThereAny(model.browseType)) { - return Container(); - } - double? itemHeight; - if (model.browseType == BrowseType.musicVideoTypeAtv.name) { - itemHeight = 174.h; - } else if (model.browseType == BrowseType.musicPageTypeAlbum.name || model.browseType == BrowseType.musicPageTypePlaylist.name) { - itemHeight = 195.h; - } else if (model.browseType == BrowseType.musicVideoTypeOmv.name) { - itemHeight = 277.h; - } - return SizedBox( - height: itemHeight, - child: Column( - children: [ - _labelWidget(ObjUtil.getStr(model.headerTitle)), - Expanded( - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: model.contents != null ? model.contents!.length : 0, - itemBuilder: (context, index) { - final content = model.contents![index]; - if (model.browseType == BrowseType.musicVideoTypeAtv.name) { - return AtvItem(content: content, onTap: () => controller.goPlayMusic(content)); - } else if (model.browseType == BrowseType.musicPageTypeAlbum.name || model.browseType == BrowseType.musicPageTypePlaylist.name) { - return AlbumItem(content: content); - } else if (model.browseType == BrowseType.musicVideoTypeOmv.name) { - return OmvItem(content: content); - } - return Container(); - }, - separatorBuilder: (context, index) { - return SizedBox(width: 7.w); - }, - ), - ), - ], - ), - ); - } } diff --git a/lib/modules/sideb/initial/initial_controller.dart b/lib/modules/sideb/initial/initial_controller.dart index d8137ee..e63849c 100644 --- a/lib/modules/sideb/initial/initial_controller.dart +++ b/lib/modules/sideb/initial/initial_controller.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:tone_snap/generated/assets.dart'; import 'package:tone_snap/modules/sideb/home/home_view.dart'; -import 'package:tone_snap/modules/sideb/me/me_view.dart'; +import 'package:tone_snap/modules/sideb/personal_music_library/personal_music_library_view.dart'; import 'package:tone_snap/modules/sideb/search_music/search_music_view.dart'; class InitialController extends GetxController { @@ -11,7 +11,7 @@ class InitialController extends GetxController { final pages = [ PageItem([Assets.sideBBnb1Unselected, Assets.sideBBnb1Selected], const HomeView()), PageItem([Assets.sideBBnb2Unselected, Assets.sideBBnb2Selected], const SearchMusicView()), - PageItem([Assets.sideBBnb3Unselected, Assets.sideBBnb3Selected], const MeView()), + PageItem([Assets.sideBBnb3Unselected, Assets.sideBBnb3Selected], const PersonalMusicLibraryView()), ]; var currentIndex = 0.obs; diff --git a/lib/modules/sideb/initial/initial_view.dart b/lib/modules/sideb/initial/initial_view.dart index ef4b3ad..deab69c 100644 --- a/lib/modules/sideb/initial/initial_view.dart +++ b/lib/modules/sideb/initial/initial_view.dart @@ -15,28 +15,35 @@ class InitialView extends StatelessWidget { double bottomPadding = MediaQuery.of(context).padding.bottom; return Stack( children: [ - Image.asset( - Assets.sideBHomeBg, - width: 1.sw, - fit: BoxFit.fitWidth, - ), + _buildPageBg(), Scaffold( backgroundColor: Colors.transparent, - body: PageView( - physics: const NeverScrollableScrollPhysics(), - controller: controller.pageController, - children: controller.pages.map((e) => KeepAliveWrapper(child: e.widget)).toList(), - ), + body: _buildBody(), bottomNavigationBar: _buildBottomAppBar(bottomPadding), ), - // _buildPlayerBar(bottomPadding), ], ); } + Widget _buildPageBg() { + return Image.asset( + Assets.sideBHomeBg, + width: 1.sw, + fit: BoxFit.fitWidth, + ); + } + + Widget _buildBody() { + return PageView( + physics: const NeverScrollableScrollPhysics(), + controller: controller.pageController, + children: controller.pages.map((e) => KeepAliveWrapper(child: e.widget)).toList(), + ); + } + Widget _buildBottomAppBar(double bottomPadding) { return Container( - height: 72.h + bottomPadding, + height: kBottomNavigationBarHeight + bottomPadding, padding: EdgeInsets.only(bottom: bottomPadding), decoration: BoxDecoration( borderRadius: BorderRadius.only(topLeft: Radius.circular(24.r), topRight: Radius.circular(24.r)), @@ -79,28 +86,4 @@ class InitialView extends StatelessWidget { ), ); } - - Widget _buildPlayerBar(double bottomPadding) { - return Positioned( - bottom: 65.5.h + bottomPadding, - left: 16.w, - right: 16.w, - child: Container( - width: 1.sw, - height: 75.5.h, - decoration: BoxDecoration( - color: const Color(0xFF80F988), - borderRadius: BorderRadius.circular(36).r, - boxShadow: const [ - BoxShadow( - color: Color(0x40040604), - offset: Offset(0, 4), - blurRadius: 4, - spreadRadius: 0, - ), - ], - ), - ), - ); - } } diff --git a/lib/modules/sideb/love_songs/love_songs_binding.dart b/lib/modules/sideb/love_songs/love_songs_binding.dart new file mode 100644 index 0000000..9172edb --- /dev/null +++ b/lib/modules/sideb/love_songs/love_songs_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'love_songs_controller.dart'; + +class LoveSongsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => LoveSongsController()); + } +} diff --git a/lib/modules/sideb/love_songs/love_songs_controller.dart b/lib/modules/sideb/love_songs/love_songs_controller.dart new file mode 100644 index 0000000..cabb5a1 --- /dev/null +++ b/lib/modules/sideb/love_songs/love_songs_controller.dart @@ -0,0 +1,13 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/storage/love_songs_box.dart'; + +class LoveSongsController extends GetxController { + var loveList = []; + + @override + void onInit() { + super.onInit(); + loveList = LoveSongsBox().getList(); + } +} diff --git a/lib/modules/sideb/love_songs/love_songs_view.dart b/lib/modules/sideb/love_songs/love_songs_view.dart new file mode 100644 index 0000000..98c2d6b --- /dev/null +++ b/lib/modules/sideb/love_songs/love_songs_view.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_scrollbar.dart'; +import 'package:tone_snap/components/network_image_widget.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart'; + +import 'love_songs_controller.dart'; + +class LoveSongsView extends StatelessWidget { + LoveSongsView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MusicAppbar( + title: 'Love Songs', + ), + _buildTotal(), + _buildListView(), + ], + ), + ), + ); + } + + Widget _buildTotal() { + return Padding( + padding: EdgeInsets.only(top: 34.h, bottom: 10.h, left: 18.w, right: 18.w), + child: Text( + '${controller.loveList.length} Songs', + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } + + Widget _buildListView() { + return Expanded( + child: BaseScrollbar( + child: ListView.builder( + itemCount: controller.loveList.length, + itemBuilder: (context, index) { + return _buildItem(controller.loveList[index]); + }, + ), + ), + ); + } + + Widget _buildItem(MusicModel model) { + return InkWell( + onTap: (){}, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 18.w), + child: Row( + children: [ + NetworkImageWidget( + url: '', + width: 60.w, + height: 60.w, + radius: 8.r, + ), + SizedBox(width: 14.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'There For You', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + SizedBox(height: 4.h), + Text( + '234 songs', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0xFF999999), + fontSize: 12.sp, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/sideb/me/me_binding.dart b/lib/modules/sideb/me/me_binding.dart deleted file mode 100644 index f0135d6..0000000 --- a/lib/modules/sideb/me/me_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; - -import 'me_controller.dart'; - -class MeBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => MeController()); - } -} diff --git a/lib/modules/sideb/me/me_controller.dart b/lib/modules/sideb/me/me_controller.dart deleted file mode 100644 index 8666234..0000000 --- a/lib/modules/sideb/me/me_controller.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:get/get.dart'; - -class MeController extends GetxController { - -} diff --git a/lib/modules/sideb/me/me_view.dart b/lib/modules/sideb/me/me_view.dart deleted file mode 100644 index 07ce00f..0000000 --- a/lib/modules/sideb/me/me_view.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import 'me_controller.dart'; - -class MeView extends GetView { - const MeView({super.key}); - - @override - Widget build(BuildContext context) { - Get.find(); - - return Container(); - } -} diff --git a/lib/modules/sideb/personal_music_library/personal_music_library_binding.dart b/lib/modules/sideb/personal_music_library/personal_music_library_binding.dart new file mode 100644 index 0000000..c51b1e8 --- /dev/null +++ b/lib/modules/sideb/personal_music_library/personal_music_library_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'personal_music_library_controller.dart'; + +class PersonalMusicLibraryBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => PersonalMusicLibraryController()); + } +} diff --git a/lib/modules/sideb/personal_music_library/personal_music_library_controller.dart b/lib/modules/sideb/personal_music_library/personal_music_library_controller.dart new file mode 100644 index 0000000..211b3d2 --- /dev/null +++ b/lib/modules/sideb/personal_music_library/personal_music_library_controller.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/modules/sideb/collect_playlists/collect_playlists_view.dart'; +import 'package:tone_snap/modules/sideb/playlists/playlists_view.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class PersonalMusicLibraryController extends GetxController with GetSingleTickerProviderStateMixin { + late TabController tabController; + final labels = ['Playlists', 'Collect Playlists']; + final pages = [const PlaylistsView(), const CollectPlaylistsView()]; + + @override + void onInit() { + super.onInit(); + tabController = TabController(length: labels.length, vsync: this); + } + + @override + void onClose() { + tabController.dispose(); + super.onClose(); + } + + void onTapModule(int index) { + if (index == 0) { + Get.toNamed(AppRoutes.loveSongs); + } + if (index == 1) { + Get.toNamed(AppRoutes.loveSongs); + } + if (index == 2) { + Get.toNamed(AppRoutes.loveSongs); + } + } +} diff --git a/lib/modules/sideb/personal_music_library/personal_music_library_view.dart b/lib/modules/sideb/personal_music_library/personal_music_library_view.dart new file mode 100644 index 0000000..a2479db --- /dev/null +++ b/lib/modules/sideb/personal_music_library/personal_music_library_view.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/keep_alive_wrapper.dart'; +import 'package:tone_snap/components/my_custom_indicator.dart'; +import 'package:tone_snap/components/network_image_widget.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; + +import 'personal_music_library_controller.dart'; + +class PersonalMusicLibraryView extends GetView { + const PersonalMusicLibraryView({super.key}); + + @override + Widget build(BuildContext context) { + Get.find(); + return Stack( + children: [ + _buildPageBg(), + SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitle(), + _buildModule(), + _buildTabBar(), + _buildTabBarView(), + ], + ), + ), + ], + ); + } + + Widget _buildPageBg() { + return Image.asset( + Assets.sideBPersonalMusicLibraryBg, + width: 1.sw, + fit: BoxFit.fitWidth, + ); + } + + Widget _buildTitle() { + return Padding( + padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 16.w), + child: Text( + 'Library', + style: TextStyle( + color: Colors.white, + fontSize: 28.sp, + ), + ), + ); + } + + Widget _buildModule() { + return Padding( + padding: EdgeInsets.symmetric(vertical: 28.h, horizontal: 16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildModuleItem('', Assets.sideBLoveSolid, 'Songs 122', () => controller.onTapModule(0)), + _buildModuleItem('', Assets.sideBArtists, 'Artists 122', () => controller.onTapModule(1)), + _buildModuleItem('', Assets.sideBOfflineDownload, 'Offline 122', () => controller.onTapModule(2)), + ], + ), + ); + } + + Widget _buildModuleItem(String url, String labelImg, String label, Function() onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 109.w, + height: 109.w, + decoration: BoxDecoration( + color: const Color(0xFF242529), + borderRadius: BorderRadius.circular(8).r, + ), + child: Stack( + children: [ + Center( + child: NetworkImageWidget( + url: url, + width: 55.w, + height: 55.w, + placeholder: Assets.sideBPlaceholderLibrary, + ), + ), + Positioned( + bottom: 8.h, + left: 8.w, + right: 8.w, + child: Row( + children: [ + Image.asset( + labelImg, + width: 20, + height: 20, + ), + SizedBox(width: 4.w), + Expanded( + child: Text( + label, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + height: 1, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildTabBar() { + return Container( + height: 32.h, + margin: const EdgeInsets.symmetric(vertical: 12).h, + child: TabBar( + controller: controller.tabController, + tabAlignment: TabAlignment.center, + dividerHeight: 0, + padding: const EdgeInsets.symmetric(horizontal: 4).w, + labelPadding: const EdgeInsets.symmetric(horizontal: 12).w, + labelStyle: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + unselectedLabelStyle: TextStyle( + color: const Color(0xFF999999), + fontSize: 14.sp, + ), + indicatorPadding: EdgeInsets.zero, + indicator: MyCustomIndicator( + indWidth: 16.w, + indHeight: 2.h, + color: seedColor, + radius: 1.r, + ), + tabs: controller.labels.map((e) => Tab(text: e)).toList(), + ), + ); + } + + Widget _buildTabBarView() { + return Expanded( + child: TabBarView( + controller: controller.tabController, + children: controller.pages.map((e) => KeepAliveWrapper(child: e)).toList(), + ), + ); + } +} diff --git a/lib/modules/sideb/play_music/play_music_binding.dart b/lib/modules/sideb/play_music/play_music_binding.dart deleted file mode 100644 index 13b41b6..0000000 --- a/lib/modules/sideb/play_music/play_music_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; - -import 'play_music_controller.dart'; - -class PlayMusicBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => PlayMusicController()); - } -} diff --git a/lib/modules/sideb/play_music/play_music_view.dart b/lib/modules/sideb/play_music/play_music_view.dart deleted file mode 100644 index e247438..0000000 --- a/lib/modules/sideb/play_music/play_music_view.dart +++ /dev/null @@ -1,317 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import 'package:tone_snap/components/full_width_track_shape.dart'; -import 'package:tone_snap/components/my_marquee_text.dart'; -import 'package:tone_snap/components/network_image_widget.dart'; -import 'package:tone_snap/generated/assets.dart'; -import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart'; -import 'package:tone_snap/res/themes/app_colors.dart'; -import 'package:tone_snap/utils/obj_util.dart'; - -import 'play_music_controller.dart'; - -class PlayMusicView extends StatelessWidget { - PlayMusicView({super.key}); - - final controller = Get.find(); - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Obx(() { - return Visibility( - visible: ObjUtil.isNotEmptyStr(controller.thumbnail.value), - child: NetworkImageWidget( - url: controller.thumbnail.value, - width: 1.sw, - height: 1.sh, - placeholder: Container(), - errorWidget: Container(), - ), - ); - }), - _buildPageBg(), - Scaffold( - backgroundColor: Colors.transparent, - body: Column( - children: [ - _buildHeader(), - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 20).w, - child: Column( - children: [ - SizedBox(height: 12.h), - _buildCover(), - SizedBox(height: 36.h), - _buildMusicName(), - SizedBox(height: 24.h), - _processBar(context), - SizedBox(height: 100.h), - _buildPlayController(), - ], - ), - ), - ), - ], - ), - ) - ], - ); - } - - /// 页面占位背景 - Widget _buildPageBg() { - return BackdropFilter( - filter: ImageFilter.blur(sigmaX: 50.0, sigmaY: 50.0), - child: Container( - width: 1.sw, - height: 1.sh, - color: const Color(0x26000000), - ), - ); - } - - /// 头部 - Widget _buildHeader() { - return MusicAppbar( - isDownBack: true, - isBackBorder: true, - actionOnTap: (){}, - titleWidget: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildMenuText('SONG', true), - Container( - width: 1.w, - height: 14.h, - color: const Color(0x73FFFFFF), - ), - _buildMenuText('LYRICS', false), - ], - ), - action: Image.asset( - Assets.sideBCrossCircle, - width: 24.w, - height: 24.w, - ), - ); - } - - /// 播放页/歌词页 - Widget _buildMenuText(String label, bool selected) { - return InkWell( - onTap: (){}, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 15.5.w, vertical: 8.h), - child: Text( - label, - style: TextStyle( - color: Color(selected ? 0xD9FFFFFF : 0x73FFFFFF), - fontSize: 15.sp, - ), - ), - ), - ); - } - - /// 封面 - Widget _buildCover() { - return Obx(() { - return NetworkImageWidget( - url: controller.thumbnail.value, - width: 1.sw, - height: 330.h, - radius: 16.r, - ); - }); - } - - /// 歌名/作者信息、收藏、下载 - Widget _buildMusicName() { - return Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Obx(() { - return MyMarqueeText( - text: ObjUtil.getStr(controller.title.value), - textStyle: TextStyle( - color: const Color(0xD9FFFFFF), - fontSize: 22.sp, - ), - ); - }), - SizedBox(height: 6.h), - Obx(() { - return MyMarqueeText( - text: ObjUtil.getStr(controller.subTitle.value), - textStyle: TextStyle( - color: const Color(0x99EEEEEE), - fontSize: 12.sp, - ), - ); - }), - ], - ), - ), - SizedBox(width: 10.w), - Image.asset( - Assets.sideBLove, - width: 24.w, - height: 24.w, - ), - SizedBox(width: 20.w), - Image.asset( - Assets.sideBDownload, - width: 24.w, - height: 24.w, - ), - ], - ); - } - - /// 进度条 - Widget _processBar(BuildContext context) { - return Column( - children: [ - SliderTheme( - data: SliderTheme.of(context).copyWith( - activeTrackColor: sideBSeedColor, - inactiveTrackColor: const Color(0x4DFFFFFF), - secondaryActiveTrackColor: sideBSeedColor.withOpacity(0.3), - trackHeight: 3.h, - thumbColor: Colors.white, - thumbShape: RoundSliderThumbShape( - disabledThumbRadius: 7.w, - enabledThumbRadius: 7.w, - ), - trackShape: FullWidthTrackShape(), - ), - child: SizedBox( - width: 1.sw, - height: 7.w, - child: Obx(() { - return Slider( - value: controller.musicPlayerController.positionDuration.value.inSeconds.toDouble(), - secondaryTrackValue: controller.musicPlayerController.bufferedDuration.value.inSeconds.toDouble(), - min: 0, - max: controller.musicPlayerController.totalDuration.value.inSeconds.toDouble(), - onChanged: (value) => controller.musicPlayerController.seekToPosition(value), - onChangeStart: (value) => controller.musicPlayerController.seekStartEnd(0), - onChangeEnd: (value) => controller.musicPlayerController.seekStartEnd(1), - ); - }), - ), - ), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Obx(() { - return Text( - controller.musicPlayerController.getPositionDuration(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: const Color(0xD9FFFFFF), - fontSize: 12.sp, - fontWeight: FontWeight.w500, - ), - ); - }), - ), - Flexible( - child: Obx(() { - return Text( - controller.musicPlayerController.getTotalDuration(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: const Color(0x99FFFFFF), - fontSize: 12.sp, - fontWeight: FontWeight.w500, - ), - ); - }), - ), - ], - ), - ], - ); - } - - /// 播放控制器 - Widget _buildPlayController() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: (){}, - child: Image.asset( - Assets.sideBListLoop, - width: 24.w, - height: 24.w, - ), - ), - GestureDetector( - onTap: controller.nextTrack, - child: Padding( - padding: const EdgeInsets.only(left: 20).w, - child: Image.asset( - Assets.sideBPreviousSong, - width: 20.w, - height: 20.w, - ), - ), - ), - GestureDetector( - onTap: controller.playPause, - child: ClipOval( - child: Container( - width: 66.w, - height: 66.w, - color: Colors.white, - child: FittedBox( - fit: BoxFit.none, - child: Obx(() { - return Image.asset( - controller.musicPlayerController.isPlaying.value ? Assets.sideBPausePlay : Assets.sideBStartPlay, - width: 26.w, - height: 26.w, - ); - }), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 20).w, - child: GestureDetector( - onTap: controller.nextTrack, - child: Image.asset( - Assets.sideBNextSong, - width: 20.w, - height: 20.w, - ), - ), - ), - GestureDetector( - onTap: (){}, - child: Image.asset( - Assets.sideBPlayList, - width: 24.w, - height: 24.w, - ), - ), - ], - ); - } -} diff --git a/lib/modules/sideb/play_page/play_page_binding.dart b/lib/modules/sideb/play_page/play_page_binding.dart new file mode 100644 index 0000000..9a5c347 --- /dev/null +++ b/lib/modules/sideb/play_page/play_page_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/sideb/play_page/play_page_controller.dart'; + +class PlayPageBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => PlayPageController()); + } +} diff --git a/lib/modules/sideb/play_music/play_music_controller.dart b/lib/modules/sideb/play_page/play_page_controller.dart similarity index 53% rename from lib/modules/sideb/play_music/play_music_controller.dart rename to lib/modules/sideb/play_page/play_page_controller.dart index 67e16d8..a351cc7 100644 --- a/lib/modules/sideb/play_music/play_music_controller.dart +++ b/lib/modules/sideb/play_page/play_page_controller.dart @@ -1,45 +1,52 @@ import 'package:get/get.dart'; import 'package:tone_snap/components/base_easyloading.dart'; -import 'package:tone_snap/data/sideb/api/music_api.dart'; -import 'package:tone_snap/data/sideb/models/next_model.dart'; -import 'package:tone_snap/data/sideb/models/play_list_model.dart'; -import 'package:tone_snap/data/sideb/models/player_model.dart'; +import 'package:tone_snap/data/api/music_api.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/models/next_model.dart'; +import 'package:tone_snap/data/storage/love_songs_box.dart'; import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart'; -import 'package:tone_snap/utils/obj_util.dart'; -class PlayMusicController extends GetxController { +class PlayPageController extends GetxController { var musicPlayerController = MusicPlayerController.to; + /// 是否从 MusicBar 打开 + bool isMusicBarOpen = false; + + /// 是否添加到喜欢 + var isLove = false.obs; + /// 当前播放的歌曲信息 late String videoId; - late String playlistId; - var thumbnail = Rx(null); - var title = Rx(null); - var subTitle = Rx(null); - - /// 播放列表 - List playList = []; - - /// 当前播放的歌曲下标 - int currentIndex = 0; + String? playlistId; + List? playList; @override void onInit() { super.onInit(); + videoId = Get.arguments['videoId'] ?? ''; playlistId = Get.arguments['playlistId']; - videoId = Get.arguments['videoId']; + playList = Get.arguments['playList']; + isMusicBarOpen = Get.arguments['isMusicBarOpen'] ?? false; + + isLove.value = LoveSongsBox().isLove(videoId); } @override void onReady() async { super.onReady(); - _next(); + if (!isMusicBarOpen) { + if (musicPlayerController.musicModel.value.videoId != videoId) { + playList ??= await _next(); + musicPlayerController.setPlayList(videoId, playList!); + musicPlayerController.playMusic(); + } + } } - Future _next() async { - BaseEasyLoading.loading(); + /// 获取播放列表 + Future> _next() async { + List playList = []; NextModel? model = await MusicApi.next(playlistId: playlistId, videoId: videoId); - BaseEasyLoading.dismiss(); if (model != null) { var tabs = model.contents?.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer?.tabs; if (tabs != null && tabs.isNotEmpty) { @@ -49,7 +56,7 @@ class PlayMusicController extends GetxController { var contents = o.tabRenderer?.content?.musicQueueRenderer?.content?.playlistPanelRenderer?.contents; if (contents != null && contents.isNotEmpty) { for (var j = 0; j < contents.length; ++j) { - var playListModel = PlayListModel(); + var playListModel = MusicModel(); var content = contents[j]; // 封面 var thumbnails = content.playlistPanelVideoRenderer?.thumbnail?.thumbnails; @@ -81,56 +88,33 @@ class PlayMusicController extends GetxController { } } } - _getCurrentMusicInfo(); - _player(); + } + return playList; + } + + /// 加入/取消喜欢 + Future addCancelLove() async { + if (isLove.value) { + await LoveSongsBox().delete(musicPlayerController.musicModel.value.videoId!); + isLove.value = false; + BaseEasyLoading.toast('Cancelled'); + } else { + await LoveSongsBox().addData(musicPlayerController.musicModel.value.copyWith()); + isLove.value = true; + BaseEasyLoading.toast('Collected'); } } - /// 获取当前下标的歌曲信息 - void _getCurrentMusicInfo() { - if (playList.isNotEmpty) { - thumbnail.value = playList[currentIndex].thumbnail; - title.value = playList[currentIndex].title; - subTitle.value = playList[currentIndex].subTitle; + /// 在播放列表中切换歌曲 + void switchMusic(int index, MusicModel model) { + if (musicPlayerController.musicModel.value.videoId != model.videoId) { + musicPlayerController.playMusicSpecifyIndex(index); } + Get.back(); } - Future _player() async { - if (playList.isEmpty) return; - BaseEasyLoading.loading(); - PlayerModel? model = await MusicApi.player(videoId: playList[currentIndex].videoId); - BaseEasyLoading.dismiss(); - if (model != null && model.streamingData != null) { - var formats = model.streamingData?.formats; - if (formats != null && playList.isNotEmpty) { - playList[currentIndex].url = formats[0].url ?? ''; - - // 开始播放 - musicPlayerController.playNewUrl(playList[currentIndex].url!, ObjUtil.getStr(playList[currentIndex].videoId)); - } - } - } - - /// 继续/暂停 - void playPause() { - musicPlayerController.playPause(); - } - - /// 上一首 - Future previousTrack() async { - if (currentIndex > 0) { - currentIndex--; - } - _getCurrentMusicInfo(); - _player(); - } - - /// 下一首 - Future nextTrack() async { - if (currentIndex < playList.length - 1) { - currentIndex++; - } - _getCurrentMusicInfo(); - _player(); + /// 删除播放列表中的歌曲 + void deleteMusic(int index) { + musicPlayerController.playList.removeAt(index); } } diff --git a/lib/modules/sideb/play_page/play_page_view.dart b/lib/modules/sideb/play_page/play_page_view.dart new file mode 100644 index 0000000..0f0dc95 --- /dev/null +++ b/lib/modules/sideb/play_page/play_page_view.dart @@ -0,0 +1,535 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:tone_snap/components/base_scrollbar.dart'; +import 'package:tone_snap/components/full_width_track_shape.dart'; +import 'package:tone_snap/components/my_marquee_text.dart'; +import 'package:tone_snap/components/network_image_widget.dart'; +import 'package:tone_snap/data/enum/play_mode.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart'; +import 'package:tone_snap/modules/sideb/play_page/play_page_controller.dart'; +import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; +import 'package:tone_snap/utils/date_util.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class PlayPageView extends StatelessWidget { + PlayPageView({super.key}); + + final controller = Get.find(); + final musicPlayerController = MusicPlayerController.to; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _buildPageBgImg(), + _buildPageBgColor(), + Scaffold( + backgroundColor: Colors.transparent, + body: Column( + children: [ + _buildHeader(), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20).w, + child: Column( + children: [ + SizedBox(height: 12.h), + _buildCover(), + SizedBox(height: 36.h), + _buildMusicName(), + SizedBox(height: 24.h), + _processBar(context), + SizedBox(height: 24.h), + ], + ), + ), + ), + _buildPlayControlButtons(), + SizedBox(height: 67.h), + ], + ), + ) + ], + ); + } + + /// 页面背景图 + Widget _buildPageBgImg() { + return Obx(() { + return Visibility( + visible: ObjUtil.isNotEmptyStr(musicPlayerController.musicModel.value.thumbnail), + child: NetworkImageWidget( + url: musicPlayerController.musicModel.value.thumbnail, + width: 1.sw, + height: 1.sh, + noPlaceholder: true, + ), + ); + }); + } + + /// 页面背景色 + Widget _buildPageBgColor() { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 50.0, sigmaY: 50.0), + child: Container( + width: 1.sw, + height: 1.sh, + color: const Color(0x26000000), + ), + ); + } + + /// 头部 + Widget _buildHeader() { + return MusicAppbar( + isDownBack: true, + isBackBorder: true, + actionOnTap: (){}, + titleWidget: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildMenuText('SONG', true), + Container( + width: 1.w, + height: 14.h, + color: const Color(0x73FFFFFF), + ), + _buildMenuText('LYRICS', false), + ], + ), + action: Image.asset( + Assets.sideBCrossCircle, + width: 24.w, + height: 24.w, + ), + ); + } + + /// 播放页/歌词页 + Widget _buildMenuText(String label, bool selected) { + return InkWell( + onTap: (){}, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 15.5.w, vertical: 8.h), + child: Text( + label, + style: TextStyle( + color: Color(selected ? 0xD9FFFFFF : 0x73FFFFFF), + fontSize: 15.sp, + ), + ), + ), + ); + } + + /// 封面 + Widget _buildCover() { + return Obx(() { + return NetworkImageWidget( + url: musicPlayerController.musicModel.value.thumbnail, + width: 1.sw, + height: 330.h, + radius: 16.r, + ); + }); + } + + /// 歌名/作者信息、收藏、下载 + Widget _buildMusicName() { + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + return MyMarqueeText( + text: ObjUtil.getStr(musicPlayerController.musicModel.value.title), + textStyle: TextStyle( + color: const Color(0xD9FFFFFF), + fontSize: 22.sp, + ), + ); + }), + SizedBox(height: 6.h), + Obx(() { + return MyMarqueeText( + text: ObjUtil.getStr(musicPlayerController.musicModel.value.subTitle), + textStyle: TextStyle( + color: const Color(0x99EEEEEE), + fontSize: 12.sp, + ), + ); + }), + ], + ), + ), + SizedBox(width: 10.w), + ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: controller.addCancelLove, + child: Padding( + padding: EdgeInsets.all(6.w), + child: Obx(() { + return Image.asset( + controller.isLove.value ? Assets.sideBLoveSolid : Assets.sideBLove, + width: 24.w, + height: 24.w, + ); + }), + ), + ), + ), + ), + SizedBox(width: 8.w), + ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: (){}, + child: Padding( + padding: EdgeInsets.all(6.w), + child: Image.asset( + Assets.sideBDownload, + width: 24.w, + height: 24.w, + ), + ), + ), + ), + ), + ], + ); + } + + /// 进度条 + Widget _processBar(BuildContext context) { + return Column( + children: [ + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: seedColor, + inactiveTrackColor: const Color(0x4DFFFFFF), + secondaryActiveTrackColor: seedColor.withOpacity(0.3), + trackHeight: 3.h, + thumbColor: Colors.white, + thumbShape: RoundSliderThumbShape( + disabledThumbRadius: 7.w, + enabledThumbRadius: 7.w, + ), + trackShape: FullWidthTrackShape(), + ), + child: SizedBox( + width: 1.sw, + height: 7.w, + child: Obx(() { + return Slider( + value: musicPlayerController.positionDuration.value.inSeconds.toDouble(), + secondaryTrackValue: musicPlayerController.bufferedDuration.value.inSeconds.toDouble(), + min: 0, + max: musicPlayerController.totalDuration.value.inSeconds.toDouble(), + onChanged: (value) => musicPlayerController.seekToPosition(value), + onChangeStart: (value) => musicPlayerController.seekStartEnd(0), + onChangeEnd: (value) => musicPlayerController.seekStartEnd(1), + ); + }), + ), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Obx(() { + return Text( + DateUtil.playDuration(musicPlayerController.positionDuration.value), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0xD9FFFFFF), + fontSize: 12.sp, + fontWeight: FontWeight.w500, + ), + ); + }), + ), + Flexible( + child: Obx(() { + return Text( + DateUtil.playDuration(musicPlayerController.totalDuration.value), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0x99FFFFFF), + fontSize: 12.sp, + fontWeight: FontWeight.w500, + ), + ); + }), + ), + ], + ), + ], + ); + } + + /// 播放控制按钮 + Widget _buildPlayControlButtons() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10).w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: musicPlayerController.switchPlayMode, + child: Obx(() { + var playModeAssets = Assets.sideBListLoop; + if (musicPlayerController.playMode.value == PlayMode.listLoop) { + playModeAssets = Assets.sideBListLoop; + } else if (musicPlayerController.playMode.value == PlayMode.random) { + playModeAssets = Assets.sideBShufflePlayback; + } else if (musicPlayerController.playMode.value == PlayMode.singleCycle) { + playModeAssets = Assets.sideBSingleCycle; + } + return Padding( + padding: const EdgeInsets.all(10).w, + child: Image.asset( + playModeAssets, + width: 24.w, + height: 24.w, + ), + ); + }), + ), + ), + ), + ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: musicPlayerController.previousTrack, + child: Padding( + padding: const EdgeInsets.all(12).w, + child: Obx(() { + return Image.asset( + Assets.sideBPreviousTrack, + width: 20.w, + height: 20.w, + color: musicPlayerController.playList.isNotEmpty ? Colors.white : const Color(0xFFa8a8a8), + ); + }), + ), + ), + ), + ), + ClipOval( + child: Material( + color: Colors.white, + child: InkWell( + onTap: musicPlayerController.playPause, + child: SizedBox( + width: 66.w, + height: 66.w, + child: FittedBox( + fit: BoxFit.none, + child: Obx(() { + return Visibility( + visible: musicPlayerController.processingState.value == ProcessingState.ready + || musicPlayerController.processingState.value == ProcessingState.completed, + replacement: Center( + child: SizedBox( + width: 20.w, + height: 20.w, + child: const CircularProgressIndicator( + color: seedColor, + strokeWidth: 3, + ), + ), + ), + child: Image.asset( + musicPlayerController.isPlaying.value ? Assets.sideBPausePlay : Assets.sideBStartPlay, + width: 26.w, + height: 26.w, + ), + ); + }), + ), + ), + ), + ), + ), + ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: musicPlayerController.nextTrack, + child: Padding( + padding: const EdgeInsets.all(12).w, + child: Obx(() { + return Image.asset( + Assets.sideBNextTrack, + width: 20.w, + height: 20.w, + color: musicPlayerController.playList.isNotEmpty ? Colors.white : const Color(0xFFa8a8a8), + ); + }), + ), + ), + ), + ), + ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: _openPlayList, + child: Padding( + padding: const EdgeInsets.all(10).w, + child: Image.asset( + Assets.sideBPlayList, + width: 24.w, + height: 24.w, + ), + ), + ), + ), + ), + ], + ), + ); + } + + void _openPlayList() { + Get.bottomSheet( + isScrollControlled:true, + Container( + width: 1.sw, + height: 0.6.sh, + decoration: BoxDecoration( + color: const Color(0xFF1A1A1A), + borderRadius: BorderRadius.only(topLeft: Radius.circular(18.r), topRight: Radius.circular(18.r)), + ), + child: Column( + children: [ + SizedBox( + width: double.infinity, + height: 30.h, + child: FittedBox( + fit: BoxFit.none, + child: Image.asset( + Assets.sideBBottomSheetIndicator, + width: 29.w, + height: 4.w, + ), + ), + ), + Expanded( + child: BaseScrollbar( + child: Obx(() { + return ListView.builder( + itemCount: musicPlayerController.playList.length, + itemBuilder: (context, index) { + return _buildPlayListItem(index, musicPlayerController.playList[index]); + }, + ); + }), + ), + ), + ], + ), + ), + ); + } + + Widget _buildPlayListItem(int index, MusicModel model) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => controller.switchMusic(index, model), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8).h, + child: Row( + children: [ + SizedBox(width: 18.w), + NetworkImageWidget( + url: model.thumbnail, + width: 60.w, + height: 60.w, + radius: 10.r, + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + return MyMarqueeText( + enable: musicPlayerController.musicModel.value.videoId == model.videoId, + text: ObjUtil.getStr(model.title), + textStyle: TextStyle( + color: musicPlayerController.musicModel.value.videoId == model.videoId + ? seedColor + : const Color(0xD9FFFFFF), + fontSize: 14.sp, + ), + ); + }), + SizedBox(height: 8.h), + Obx(() { + return MyMarqueeText( + enable: musicPlayerController.musicModel.value.videoId == model.videoId, + text: ObjUtil.getStr(model.subTitle), + textStyle: TextStyle( + color: musicPlayerController.musicModel.value.videoId == model.videoId + ? seedColor + : const Color(0x99FFFFFF), + fontSize: 12.sp, + ), + ); + }), + ], + ), + ), + Obx(() { + return Visibility( + visible: musicPlayerController.musicModel.value.videoId != model.videoId, + child: ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => controller.deleteMusic(index), + child: Padding( + padding: const EdgeInsets.all(10).w, + child: Image.asset( + Assets.sideBPlayListDelete, + width: 24.w, + height: 24.w, + ), + ), + ), + ), + ), + ); + }), + Obx(() { + return SizedBox(width: musicPlayerController.musicModel.value.videoId != model.videoId ? 8.w : 16.w); + }), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/sideb/playlists/playlists_binding.dart b/lib/modules/sideb/playlists/playlists_binding.dart new file mode 100644 index 0000000..ac75a25 --- /dev/null +++ b/lib/modules/sideb/playlists/playlists_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'playlists_controller.dart'; + +class PlaylistsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => PlaylistsController()); + } +} diff --git a/lib/modules/sideb/playlists/playlists_controller.dart b/lib/modules/sideb/playlists/playlists_controller.dart new file mode 100644 index 0000000..3dbdd7d --- /dev/null +++ b/lib/modules/sideb/playlists/playlists_controller.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class PlaylistsController extends GetxController { + ScrollController scrollController = ScrollController(); + + @override + void onClose() { + scrollController.dispose(); + super.onClose(); + } +} \ No newline at end of file diff --git a/lib/modules/sideb/playlists/playlists_view.dart b/lib/modules/sideb/playlists/playlists_view.dart new file mode 100644 index 0000000..2aeca76 --- /dev/null +++ b/lib/modules/sideb/playlists/playlists_view.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_scrollbar.dart'; +import 'package:tone_snap/components/network_image_widget.dart'; +import 'package:tone_snap/generated/assets.dart'; + +import 'playlists_controller.dart'; + +class PlaylistsView extends GetView { + const PlaylistsView({super.key}); + + @override + Widget build(BuildContext context) { + Get.find(); + + return Column( + children: [ + _buildAddPlaylists(), + _buildListView(), + ], + ); + } + + Widget _buildAddPlaylists() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 16.w), + Expanded( + child: Text( + 'Playlists', + style: TextStyle( + color: const Color(0xFF999999), + fontSize: 14.sp, + ), + ), + ), + ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () {}, + child: Padding( + padding: const EdgeInsets.all(10).w, + child: Image.asset( + Assets.sideBPlaylistsAdd, + width: 20.w, + height: 20.w, + ), + ), + ), + ), + ), + SizedBox(width: 6.w), + ], + ); + } + + Widget _buildListView() { + return Expanded( + child: BaseScrollbar( + scrollController: controller.scrollController, + child: ListView.builder( + controller: controller.scrollController, + itemCount: 10, + itemBuilder: (context, index) { + return _buildItem(); + }, + ), + ), + ); + } + + Widget _buildItem() { + return InkWell( + onTap: (){}, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 16.w), + child: Row( + children: [ + NetworkImageWidget( + url: '', + width: 60.w, + height: 60.w, + radius: 8.r, + ), + SizedBox(width: 14.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'There For You', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + ), + ), + SizedBox(height: 4.h), + Text( + '234 songs', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0xFF999999), + fontSize: 12.sp, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/sideb/splash/splash_binding.dart b/lib/modules/sideb/splash/splash_binding.dart deleted file mode 100644 index 71f96fb..0000000 --- a/lib/modules/sideb/splash/splash_binding.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:get/get.dart'; -import 'package:tone_snap/modules/sideb/splash/splash_controller.dart'; - -class SplashBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => SplashController()); - } -} diff --git a/lib/modules/sideb/splash/splash_view.dart b/lib/modules/sideb/splash/splash_view.dart deleted file mode 100644 index 46deafa..0000000 --- a/lib/modules/sideb/splash/splash_view.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import 'package:tone_snap/generated/assets.dart'; -import 'package:tone_snap/modules/sideb/splash/splash_controller.dart'; -import 'package:tone_snap/res/themes/app_colors.dart'; - -class SplashView extends StatelessWidget { - SplashView({super.key}); - - final controller = Get.find(); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - _buildImageBg(), - _buildProgress(), - ], - ), - ); - } - - Widget _buildImageBg() { - return Image.asset( - Assets.sideBLaunchImage, - width: 1.sw, - height: 1.sh, - fit: BoxFit.cover, - ); - } - - Widget _buildProgress() { - return Container( - alignment: Alignment.center, - margin: const EdgeInsets.only(bottom: 60).h, - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox( - width: 0.5.sw, - child: Obx(() { - return LinearProgressIndicator( - value: controller.processValue.value, - backgroundColor: Colors.white, - valueColor: const AlwaysStoppedAnimation(sideBSeedColor), - borderRadius: BorderRadius.circular(8).r, - ); - }), - ), - SizedBox(height: 14.h), - Text( - 'Resource Loading...', - style: TextStyle( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ); - } -} diff --git a/lib/modules/sideb/widgets/album_item.dart b/lib/modules/sideb/widgets/album_item.dart index 26c6a9a..8780045 100644 --- a/lib/modules/sideb/widgets/album_item.dart +++ b/lib/modules/sideb/widgets/album_item.dart @@ -5,18 +5,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:tone_snap/components/network_image_widget.dart'; -import 'package:tone_snap/data/sideb/models/home_model.dart'; +import 'package:tone_snap/data/models/home_model.dart'; import 'package:tone_snap/utils/obj_util.dart'; class AlbumItem extends StatelessWidget { - const AlbumItem({super.key, required this.content}); + const AlbumItem({super.key, required this.content, required this.onTap}); final Content content; + final Function() onTap; @override Widget build(BuildContext context) { return GestureDetector( - onTap: () {}, + onTap: onTap, child: SizedBox( width: 109.w, height: double.infinity, diff --git a/lib/modules/sideb/widgets/atv_item.dart b/lib/modules/sideb/widgets/atv_item.dart index d0c599f..817d6b6 100644 --- a/lib/modules/sideb/widgets/atv_item.dart +++ b/lib/modules/sideb/widgets/atv_item.dart @@ -7,7 +7,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:tone_snap/components/network_image_widget.dart'; -import 'package:tone_snap/data/sideb/models/home_model.dart'; +import 'package:tone_snap/data/models/home_model.dart'; import 'package:tone_snap/generated/assets.dart'; import 'package:tone_snap/utils/obj_util.dart'; diff --git a/lib/modules/sideb/widgets/omv_item.dart b/lib/modules/sideb/widgets/omv_item.dart index 6a98ffb..dd97f0c 100644 --- a/lib/modules/sideb/widgets/omv_item.dart +++ b/lib/modules/sideb/widgets/omv_item.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:tone_snap/components/network_image_widget.dart'; -import 'package:tone_snap/data/sideb/models/home_model.dart'; +import 'package:tone_snap/data/models/home_model.dart'; import 'package:tone_snap/utils/obj_util.dart'; class OmvItem extends StatelessWidget { diff --git a/lib/modules/sidea/splash/splash_binding.dart b/lib/modules/splash/splash_binding.dart similarity index 68% rename from lib/modules/sidea/splash/splash_binding.dart rename to lib/modules/splash/splash_binding.dart index 0a2a8c6..d89f924 100644 --- a/lib/modules/sidea/splash/splash_binding.dart +++ b/lib/modules/splash/splash_binding.dart @@ -1,5 +1,5 @@ import 'package:get/get.dart'; -import 'package:tone_snap/modules/sidea/splash/splash_controller.dart'; +import 'package:tone_snap/modules/splash/splash_controller.dart'; class SplashBinding extends Bindings { @override diff --git a/lib/modules/sideb/splash/splash_controller.dart b/lib/modules/splash/splash_controller.dart similarity index 69% rename from lib/modules/sideb/splash/splash_controller.dart rename to lib/modules/splash/splash_controller.dart index cfefa3e..3d84f21 100644 --- a/lib/modules/sideb/splash/splash_controller.dart +++ b/lib/modules/splash/splash_controller.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:tone_snap/data/app_config.dart'; +import 'package:tone_snap/data/api/tikustok_api.dart'; +import 'package:tone_snap/data/enum/app_side_enum.dart'; import 'package:tone_snap/data/models/base_model.dart'; -import 'package:tone_snap/data/sideb/api/tikustok_api.dart'; -import 'package:tone_snap/data/sideb/models/isocode_model.dart'; +import 'package:tone_snap/data/models/isocode_model.dart'; +import 'package:tone_snap/global/app_config.dart'; +import 'package:tone_snap/global/app_tracking_transparency_manager.dart'; import 'package:tone_snap/routes/app_routes.dart'; class SplashController extends GetxController with GetSingleTickerProviderStateMixin { @@ -24,12 +26,16 @@ class SplashController extends GetxController with GetSingleTickerProviderStateM ..addListener(() { processValue.value = _animation.value; if (processValue.value >= 1) { + // 显示开屏广告 + // AppOpenAdManager().showAdIfAvailable(onTap: _openInitial); _openInitial(); } }); // 启动动画 _controller.forward(); + + AppTrackingTransparencyManager().requestATT(); } @override @@ -38,6 +44,12 @@ class SplashController extends GetxController with GetSingleTickerProviderStateM _getIp(); } + @override + void onClose() { + _controller.dispose(); + super.onClose(); + } + /// 获取所在区域、ip Future _getIp() async { BaseModel? model = await TikUsTokApi.getIp(); @@ -46,14 +58,8 @@ class SplashController extends GetxController with GetSingleTickerProviderStateM } } - @override - void onClose() { - _controller.dispose(); - super.onClose(); - } - - /// 打开初始页面 + /// 打开首个页面 void _openInitial() { - Get.offNamed(AppRoutes.initialB); + Get.offNamed(AppConfig.appSideEnum == AppSideEnum.sideA ? AppRoutes.initialA : AppRoutes.initialB); } } diff --git a/lib/modules/sidea/splash/splash_view.dart b/lib/modules/splash/splash_view.dart similarity index 94% rename from lib/modules/sidea/splash/splash_view.dart rename to lib/modules/splash/splash_view.dart index 7f1bd90..e98f7e9 100644 --- a/lib/modules/sidea/splash/splash_view.dart +++ b/lib/modules/splash/splash_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:tone_snap/generated/assets.dart'; -import 'package:tone_snap/modules/sidea/splash/splash_controller.dart'; +import 'package:tone_snap/modules/splash/splash_controller.dart'; import 'package:tone_snap/res/themes/app_colors.dart'; class SplashView extends StatelessWidget { @@ -44,7 +44,7 @@ class SplashView extends StatelessWidget { return LinearProgressIndicator( value: controller.processValue.value, backgroundColor: Colors.white, - valueColor: const AlwaysStoppedAnimation(sideBSeedColor), + valueColor: const AlwaysStoppedAnimation(seedColor), borderRadius: BorderRadius.circular(8).r, ); }), diff --git a/lib/modules/web_page/web_page_view.dart b/lib/modules/web_page/web_page_view.dart index 6890b9d..2ce7992 100644 --- a/lib/modules/web_page/web_page_view.dart +++ b/lib/modules/web_page/web_page_view.dart @@ -18,6 +18,7 @@ class WebPageView extends StatelessWidget { body: Obx(() { return ViewStateWidget( viewState: controller.viewState.value, + cpiBgColor: Colors.white, child: WebViewWidget(controller: controller.webViewController), ); }), diff --git a/lib/res/themes/app_colors.dart b/lib/res/themes/app_colors.dart index 091e899..da54dd4 100644 --- a/lib/res/themes/app_colors.dart +++ b/lib/res/themes/app_colors.dart @@ -5,7 +5,5 @@ import 'package:flutter/material.dart'; /// 种子颜色 -const sideASeedColor = Color(0xFF000000); - -/// 种子颜色 -const sideBSeedColor = Color(0xFF80F988); \ No newline at end of file +const seedColor = Color(0xFF80F988); +const scaffoldBgColor = Color(0xFF151718); \ No newline at end of file diff --git a/lib/res/themes/app_sizes.dart b/lib/res/themes/app_sizes.dart new file mode 100644 index 0000000..008549a --- /dev/null +++ b/lib/res/themes/app_sizes.dart @@ -0,0 +1,13 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 大小 + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +/// 音乐底部播放栏高度 +double musicBarHeight = 73.5.h; + +double paddingBottomMusicBarHeight(BuildContext context) { + return musicBarHeight + MediaQuery.of(context).padding.bottom; +} \ No newline at end of file diff --git a/lib/res/themes/app_themes.dart b/lib/res/themes/app_themes.dart index 0e4f6ae..9130a5f 100644 --- a/lib/res/themes/app_themes.dart +++ b/lib/res/themes/app_themes.dart @@ -7,9 +7,9 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:tone_snap/res/themes/app_colors.dart'; ThemeData sideATheme = ThemeData.dark(useMaterial3: true).copyWith( - colorScheme: ColorScheme.fromSeed(seedColor: sideASeedColor), - appBarTheme: const AppBarTheme(color: sideASeedColor), - scaffoldBackgroundColor: sideASeedColor, + colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF000000)), + appBarTheme: const AppBarTheme(color: Color(0xFF000000)), + scaffoldBackgroundColor: const Color(0xFF000000), scrollbarTheme: ScrollbarThemeData( thumbVisibility: WidgetStateProperty.all(false), thumbColor: WidgetStateProperty.all(Colors.white), @@ -18,7 +18,7 @@ ThemeData sideATheme = ThemeData.dark(useMaterial3: true).copyWith( ); ThemeData sideBTheme = ThemeData.dark(useMaterial3: true).copyWith( - colorScheme: ColorScheme.fromSeed(seedColor: sideBSeedColor), - appBarTheme: const AppBarTheme(color: Color(0xFF151718)), - scaffoldBackgroundColor: const Color(0xFF151718), + colorScheme: ColorScheme.fromSeed(seedColor: seedColor), + appBarTheme: const AppBarTheme(color: scaffoldBgColor), + scaffoldBackgroundColor: scaffoldBgColor, ); diff --git a/lib/res/values/strings.dart b/lib/res/values/strings.dart deleted file mode 100644 index b5b976f..0000000 --- a/lib/res/values/strings.dart +++ /dev/null @@ -1,5 +0,0 @@ -// Author: fengshengxiong -// Date: 2024/5/7 -// Description: 文本 - -const appName = 'ToneSnap'; \ No newline at end of file diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 117fd09..d0dbfea 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -11,7 +11,7 @@ import 'package:tone_snap/modules/sidea/change_voice/change_voice_view.dart'; import 'package:tone_snap/modules/sidea/home/home_binding.dart' as home_binding_a; import 'package:tone_snap/modules/sidea/initial/initial_binding.dart' as initial_binding_a; import 'package:tone_snap/modules/sidea/initial/initial_view.dart' as initial_view_a; -import 'package:tone_snap/modules/sidea/me/me_binding.dart' as me_binding_a; +import 'package:tone_snap/modules/sidea/me/me_binding.dart'; import 'package:tone_snap/modules/sidea/play_sound/play_sound_binding.dart'; import 'package:tone_snap/modules/sidea/play_sound/play_sound_view.dart'; import 'package:tone_snap/modules/sidea/record_sound/record_sound_binding.dart'; @@ -19,17 +19,21 @@ import 'package:tone_snap/modules/sidea/record_sound/record_sound_view.dart'; import 'package:tone_snap/modules/sidea/settings/settings_binding.dart' as settings_binding_a; import 'package:tone_snap/modules/sidea/upload_method/upload_mothod_binding.dart'; import 'package:tone_snap/modules/sidea/upload_method/upload_mothod_view.dart'; +import 'package:tone_snap/modules/sideb/album/album_binding.dart'; +import 'package:tone_snap/modules/sideb/album/album_view.dart'; +import 'package:tone_snap/modules/sideb/collect_playlists/collect_playlists_binding.dart'; import 'package:tone_snap/modules/sideb/home/home_binding.dart' as home_binding_b; import 'package:tone_snap/modules/sideb/initial/initial_binding.dart' as initial_binding_b; import 'package:tone_snap/modules/sideb/initial/initial_view.dart' as initial_view_b; -import 'package:tone_snap/modules/sideb/me/me_binding.dart' as me_binding_b; -import 'package:tone_snap/modules/sideb/play_music/play_music_binding.dart'; -import 'package:tone_snap/modules/sideb/play_music/play_music_view.dart'; +import 'package:tone_snap/modules/sideb/love_songs/love_songs_binding.dart'; +import 'package:tone_snap/modules/sideb/love_songs/love_songs_view.dart'; +import 'package:tone_snap/modules/sideb/personal_music_library/personal_music_library_binding.dart'; +import 'package:tone_snap/modules/sideb/play_page/play_page_binding.dart'; +import 'package:tone_snap/modules/sideb/play_page/play_page_view.dart'; +import 'package:tone_snap/modules/sideb/playlists/playlists_binding.dart'; import 'package:tone_snap/modules/sideb/search_music/search_music_binding.dart'; -import 'package:tone_snap/modules/sidea/splash/splash_binding.dart' as splash_binding_a; -import 'package:tone_snap/modules/sidea/splash/splash_view.dart' as splash_view_a; -import 'package:tone_snap/modules/sideb/splash/splash_binding.dart' as splash_binding_b; -import 'package:tone_snap/modules/sideb/splash/splash_view.dart' as splash_view_b; +import 'package:tone_snap/modules/splash/splash_binding.dart'; +import 'package:tone_snap/modules/splash/splash_view.dart'; import 'package:tone_snap/modules/web_page/web_page_binding.dart'; import 'package:tone_snap/modules/web_page/web_page_view.dart'; import 'package:tone_snap/routes/app_routes.dart'; @@ -38,16 +42,22 @@ class AppPages { AppPages._(); static final routes = [ - /// SideA GetPage( - name: AppRoutes.splashA, - page: () => splash_view_a.SplashView(), - binding: splash_binding_a.SplashBinding(), + name: AppRoutes.splash, + page: () => SplashView(), + binding: SplashBinding(), ), + + /// SideA GetPage( name: AppRoutes.initialA, page: () => initial_view_a.InitialView(), - bindings: [initial_binding_a.InitialBinding(), home_binding_a.HomeBinding(), me_binding_a.MeBinding(), settings_binding_a.SettingsBinding()], + bindings: [ + initial_binding_a.InitialBinding(), + home_binding_a.HomeBinding(), + MeBinding(), + settings_binding_a.SettingsBinding(), + ], ), GetPage( name: AppRoutes.uploadMethod, @@ -89,23 +99,35 @@ class AppPages { ), /// SideB - GetPage( - name: AppRoutes.splashB, - page: () => splash_view_b.SplashView(), - binding: splash_binding_b.SplashBinding(), - ), GetPage( name: AppRoutes.initialB, page: () => initial_view_b.InitialView(), - bindings: [initial_binding_b.InitialBinding(), home_binding_b.HomeBinding(), SearchMusicBinding(), me_binding_b.MeBinding()], + bindings: [ + initial_binding_b.InitialBinding(), + home_binding_b.HomeBinding(), + SearchMusicBinding(), + PersonalMusicLibraryBinding(), + PlaylistsBinding(), + CollectPlaylistsBinding(), + ], ), GetPage( - name: AppRoutes.playMusic, - page: () => PlayMusicView(), - binding: PlayMusicBinding(), + name: AppRoutes.playPage, + page: () => PlayPageView(), + binding: PlayPageBinding(), transitionDuration: const Duration(milliseconds: 200), transition: Transition.downToUp, curve: Curves.easeIn, ), + GetPage( + name: AppRoutes.album, + page: () => AlbumView(), + binding: AlbumBinding(), + ), + GetPage( + name: AppRoutes.loveSongs, + page: () => LoveSongsView(), + binding: LoveSongsBinding(), + ), ]; } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index cf89b0f..12d1f59 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -5,8 +5,9 @@ class AppRoutes { AppRoutes._(); + static const splash = '/splash'; + /// SideA - static const splashA = '/splashA'; static const initialA = '/initialA'; static const uploadMethod = '/upload_method'; static const recordSound = '/record_sound'; @@ -17,7 +18,8 @@ class AppRoutes { static const terms = '/terms'; /// SideB - static const splashB = '/splashB'; static const initialB = '/initialB'; - static const playMusic = '/play_music'; + static const playPage = '/play_page'; + static const album = '/album'; + static const loveSongs = '/love_songs'; } diff --git a/lib/utils/device_info_util.dart b/lib/utils/device_info_util.dart index ae922cf..aeb1d09 100644 --- a/lib/utils/device_info_util.dart +++ b/lib/utils/device_info_util.dart @@ -12,10 +12,10 @@ class DeviceInfoUtil { return androidInfo.version.sdkInt; } - // /// 获取当前iOS设备的系统版本 - // static Future getIOSSystemVersion() async { - // DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); - // IosDeviceInfo iosDeviceInfo = await deviceInfoPlugin.iosInfo; - // return iosDeviceInfo.systemVersion; - // } + /// 获取当前iOS设备的系统版本 + static Future getIOSSystemVersion() async { + DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + IosDeviceInfo iosDeviceInfo = await deviceInfoPlugin.iosInfo; + return iosDeviceInfo.systemVersion; + } } diff --git a/lib/utils/num_util.dart b/lib/utils/num_util.dart index 2cc1f4b..9795246 100644 --- a/lib/utils/num_util.dart +++ b/lib/utils/num_util.dart @@ -2,6 +2,8 @@ // Date: 2024/5/7 // Description: 整数、浮点数工具类 +import 'dart:math'; + class NumUtil { static int getInt(num? value) { if (value == null) return 0; @@ -25,4 +27,20 @@ class NumUtil { static double strToDouble(String valueStr, {double defValue = 0.0}) { return double.tryParse(valueStr) ?? defValue; } + + /// 获取一个随机随 + static int getRandomNumber(int min, int max) { + Random random = Random(); + int randomNumber = random.nextInt(max - min) + min; + return randomNumber; + } + + /// 获取不包括当前的随机数 + static int getRandomNumberExcludingCurrent(int min, int max, int current) { + int newNumber; + do { + newNumber = getRandomNumber(min, max); + } while (newNumber == current); + return newNumber; + } } \ No newline at end of file diff --git a/lib/utils/tracking_authorization_util.dart b/lib/utils/tracking_authorization_util.dart deleted file mode 100644 index 075fdc3..0000000 --- a/lib/utils/tracking_authorization_util.dart +++ /dev/null @@ -1,38 +0,0 @@ -// Author: fengshengxiong -// Date: 2024/6/26 -// Description: iOS跟踪授权工具 - -import 'dart:io'; - -import 'package:app_tracking_transparency/app_tracking_transparency.dart'; -import 'package:get/get.dart'; -import 'package:tone_snap/components/dialog/remind_dialog.dart'; -import 'package:tone_snap/utils/log_util.dart'; - -class TrackingAuthorizationUtil { - /// 请求跟踪授权 - static Future requestTrackingAuthorization() async { - if (Platform.isIOS) { - final TrackingStatus status = await AppTrackingTransparency.trackingAuthorizationStatus; - if (status == TrackingStatus.notDetermined) { - // await _showCustomTrackingDialog(); - // await Future.delayed(const Duration(milliseconds: 200)); - final TrackingStatus status = await AppTrackingTransparency.requestTrackingAuthorization(); - LogUtil.d('跟踪授权状态: $status'); - } - } - } - - static Future _showCustomTrackingDialog() async { - await Get.dialog( - barrierDismissible: false, - RemindDialog( - title: 'Dear User', - content: 'We need your permission to access the advertising identifier to provide better ad services.', - confirmText: 'Continue', - showCancelBtn: false, - confirmOnTap: Get.back, - ), - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 91af587..1794c02 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.3+12 +version: 1.0.3+13 environment: sdk: '>=3.4.1 <4.0.0'