diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml index 15501a9..85d0857 100644 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -6,6 +6,6 @@ + android:src="@mipmap/launch_bg" /> \ No newline at end of file diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 47f03ce..55cdf4b 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -6,6 +6,6 @@ + android:src="@mipmap/launch_bg" /> diff --git a/android/app/src/main/res/mipmap-xhdpi/launch_bg.png b/android/app/src/main/res/mipmap-xhdpi/launch_bg.png new file mode 100755 index 0000000..665c69d Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launch_bg.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launch_image.png b/android/app/src/main/res/mipmap-xhdpi/launch_image.png deleted file mode 100755 index c57fd06..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/launch_image.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launch_bg.png b/android/app/src/main/res/mipmap-xxhdpi/launch_bg.png new file mode 100755 index 0000000..7473648 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launch_bg.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launch_image.png b/android/app/src/main/res/mipmap-xxhdpi/launch_image.png deleted file mode 100755 index b00732b..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/launch_image.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launch_bg.png b/android/app/src/main/res/mipmap-xxxhdpi/launch_bg.png new file mode 100755 index 0000000..8cf0b74 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launch_bg.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launch_image.png b/android/app/src/main/res/mipmap-xxxhdpi/launch_image.png deleted file mode 100755 index 6550306..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/launch_image.png and /dev/null differ diff --git a/android/settings.gradle b/android/settings.gradle index 536165d..3b2fdaa 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,7 +19,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false +// id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "org.jetbrains.kotlin.android" version "1.9.20" apply false } include ":app" diff --git a/assets/images/side_a/launch_bg.png b/assets/images/side_a/launch_bg.png new file mode 100755 index 0000000..7473648 Binary files /dev/null and b/assets/images/side_a/launch_bg.png differ diff --git a/assets/images/side_a/launch_image.png b/assets/images/side_a/launch_image.png deleted file mode 100755 index c57fd06..0000000 Binary files a/assets/images/side_a/launch_image.png and /dev/null differ diff --git a/assets/images/side_b/add_to_playlist.png b/assets/images/side_b/add_to_playlist.png new file mode 100755 index 0000000..211256a Binary files /dev/null and b/assets/images/side_b/add_to_playlist.png differ diff --git a/assets/images/side_b/add_to_queue.png b/assets/images/side_b/add_to_queue.png new file mode 100755 index 0000000..64b39e3 Binary files /dev/null and b/assets/images/side_b/add_to_queue.png differ diff --git a/assets/images/side_b/collected.png b/assets/images/side_b/collected.png new file mode 100644 index 0000000..0fb8b43 Binary files /dev/null and b/assets/images/side_b/collected.png differ diff --git a/assets/images/side_b/delete_history.png b/assets/images/side_b/delete_history.png new file mode 100644 index 0000000..3dc7162 Binary files /dev/null and b/assets/images/side_b/delete_history.png differ diff --git a/assets/images/side_b/delete_white.png b/assets/images/side_b/delete_white.png new file mode 100644 index 0000000..26e1893 Binary files /dev/null and b/assets/images/side_b/delete_white.png differ diff --git a/assets/images/side_b/downloaded.png b/assets/images/side_b/downloaded.png new file mode 100644 index 0000000..dcb290c Binary files /dev/null and b/assets/images/side_b/downloaded.png differ diff --git a/assets/images/side_b/empty.jpg b/assets/images/side_b/empty.jpg new file mode 100644 index 0000000..9794e37 Binary files /dev/null and b/assets/images/side_b/empty.jpg differ diff --git a/assets/images/side_b/img_error.png b/assets/images/side_b/img_error.png deleted file mode 100644 index 7ef1e09..0000000 Binary files a/assets/images/side_b/img_error.png and /dev/null differ diff --git a/assets/images/side_b/img_placeholder.png b/assets/images/side_b/img_placeholder.png deleted file mode 100644 index f7a5faf..0000000 Binary files a/assets/images/side_b/img_placeholder.png and /dev/null differ diff --git a/assets/images/side_b/love_songs_bg.png b/assets/images/side_b/love_songs_bg.png new file mode 100644 index 0000000..452e396 Binary files /dev/null and b/assets/images/side_b/love_songs_bg.png differ diff --git a/assets/images/side_b/more.png b/assets/images/side_b/more.png new file mode 100644 index 0000000..9e29af0 Binary files /dev/null and b/assets/images/side_b/more.png differ diff --git a/assets/images/side_b/more_edit.png b/assets/images/side_b/more_edit.png new file mode 100644 index 0000000..f7db255 Binary files /dev/null and b/assets/images/side_b/more_edit.png differ diff --git a/assets/images/side_b/more_remove.png b/assets/images/side_b/more_remove.png new file mode 100644 index 0000000..f9e9359 Binary files /dev/null and b/assets/images/side_b/more_remove.png differ diff --git a/assets/images/side_b/music_bar_next.png b/assets/images/side_b/music_bar_next.png new file mode 100644 index 0000000..91f1f1b Binary files /dev/null and b/assets/images/side_b/music_bar_next.png differ diff --git a/assets/images/side_b/music_placeholder.png b/assets/images/side_b/music_placeholder.png new file mode 100644 index 0000000..df530de Binary files /dev/null and b/assets/images/side_b/music_placeholder.png differ diff --git a/assets/images/side_b/download.png b/assets/images/side_b/not_download1.png similarity index 100% rename from assets/images/side_b/download.png rename to assets/images/side_b/not_download1.png diff --git a/assets/images/side_b/not_download2.png b/assets/images/side_b/not_download2.png new file mode 100644 index 0000000..d6063ec Binary files /dev/null and b/assets/images/side_b/not_download2.png differ diff --git a/assets/images/side_b/personal_music_library_bg.png b/assets/images/side_b/personal_music_library_bg.png index fbbab66..bb85ba4 100644 Binary files a/assets/images/side_b/personal_music_library_bg.png and b/assets/images/side_b/personal_music_library_bg.png differ diff --git a/assets/images/side_b/playlist_play_all.png b/assets/images/side_b/playlist_play_all.png new file mode 100644 index 0000000..413ee72 Binary files /dev/null and b/assets/images/side_b/playlist_play_all.png differ diff --git a/assets/images/side_b/album_total.png b/assets/images/side_b/playlist_play_all_random.png similarity index 100% rename from assets/images/side_b/album_total.png rename to assets/images/side_b/playlist_play_all_random.png diff --git a/assets/images/side_b/album_title_bg.png b/assets/images/side_b/playlist_title_bg.png similarity index 100% rename from assets/images/side_b/album_title_bg.png rename to assets/images/side_b/playlist_title_bg.png diff --git a/assets/images/side_b/privacy_policy.png b/assets/images/side_b/privacy_policy.png new file mode 100644 index 0000000..691f0f3 Binary files /dev/null and b/assets/images/side_b/privacy_policy.png differ diff --git a/assets/images/side_b/report.png b/assets/images/side_b/report.png new file mode 100755 index 0000000..b6ac8c9 Binary files /dev/null and b/assets/images/side_b/report.png differ diff --git a/assets/images/side_b/search.png b/assets/images/side_b/search.png new file mode 100644 index 0000000..39c7022 Binary files /dev/null and b/assets/images/side_b/search.png differ diff --git a/assets/images/side_b/search_white.png b/assets/images/side_b/search_white.png new file mode 100644 index 0000000..1295d88 Binary files /dev/null and b/assets/images/side_b/search_white.png differ diff --git a/assets/images/side_b/setting_bg.png b/assets/images/side_b/setting_bg.png new file mode 100644 index 0000000..ff2825d Binary files /dev/null and b/assets/images/side_b/setting_bg.png differ diff --git a/assets/images/side_b/terms_of_service.png b/assets/images/side_b/terms_of_service.png new file mode 100644 index 0000000..cea8b79 Binary files /dev/null and b/assets/images/side_b/terms_of_service.png differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6e9af27..a985f8a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -503,7 +503,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.tone.music.offline; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = tone_snap; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ToneSnap_DEV; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -702,7 +702,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.tone.music.offline; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = tone_snap; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ToneSnap_DEV; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -736,7 +736,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.tone.music.offline; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = tone_snap; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ToneSnap_DEV; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 497328a..d0021ba 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -51,7 +51,7 @@ ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) + GADApplicationIdentifier + ca-app-pub-5684307632319406~7113477061 LSRequiresIPhoneOS + NSAppleMusicUsageDescription + We need to access the device media library to select audio files. NSMicrophoneUsageDescription We need to access the microphone to record or select audio files. NSPhotoLibraryUsageDescription We need access to the photo library to pick audio files. - NSAppleMusicUsageDescription - We need to access the device media library to select audio files. - NSUserTrackingUsageDescription - We need your permission to access the advertising identifier to provide better ad services. + NSUserTrackingUsageDescription + We need your permission to access the advertising identifier to provide better ad services. + SKAdNetworkItems + + + SKAdNetworkIdentifier + cstr6suwn9.skadnetwork + + + SKAdNetworkIdentifier + 4fzdc2evr5.skadnetwork + + + SKAdNetworkIdentifier + 4pfyvq9l8r.skadnetwork + + + SKAdNetworkIdentifier + 2fnua5tdw4.skadnetwork + + + SKAdNetworkIdentifier + ydx93a7ass.skadnetwork + + + SKAdNetworkIdentifier + 5a6flpkh64.skadnetwork + + + SKAdNetworkIdentifier + p78axxw29g.skadnetwork + + + SKAdNetworkIdentifier + v72qych5uu.skadnetwork + + + SKAdNetworkIdentifier + ludvb6z3bs.skadnetwork + + + SKAdNetworkIdentifier + cp8zw746q7.skadnetwork + + + SKAdNetworkIdentifier + 3sh42y64q3.skadnetwork + + + SKAdNetworkIdentifier + c6k4g5qg8m.skadnetwork + + + SKAdNetworkIdentifier + s39g8k73mm.skadnetwork + + + SKAdNetworkIdentifier + 3qy4746246.skadnetwork + + + SKAdNetworkIdentifier + f38h382jlk.skadnetwork + + + SKAdNetworkIdentifier + hs6bdukanm.skadnetwork + + + SKAdNetworkIdentifier + v4nxqhlyqp.skadnetwork + + + SKAdNetworkIdentifier + wzmmz9fp6w.skadnetwork + + + SKAdNetworkIdentifier + yclnxrl5pm.skadnetwork + + + SKAdNetworkIdentifier + t38b2kh725.skadnetwork + + + SKAdNetworkIdentifier + 7ug5zh24hu.skadnetwork + + + SKAdNetworkIdentifier + gta9lk7p23.skadnetwork + + + SKAdNetworkIdentifier + vutu7akeur.skadnetwork + + + SKAdNetworkIdentifier + y5ghdn5j9k.skadnetwork + + + SKAdNetworkIdentifier + n6fk4nfna4.skadnetwork + + + SKAdNetworkIdentifier + v9wttpbfk9.skadnetwork + + + SKAdNetworkIdentifier + n38lu8286q.skadnetwork + + + SKAdNetworkIdentifier + 47vhws6wlr.skadnetwork + + + SKAdNetworkIdentifier + kbd757ywx3.skadnetwork + + + SKAdNetworkIdentifier + 9t245vhmpl.skadnetwork + + + SKAdNetworkIdentifier + eh6m2bh4zr.skadnetwork + + + SKAdNetworkIdentifier + a2p9lx4jpn.skadnetwork + + + SKAdNetworkIdentifier + 22mmun2rn5.skadnetwork + + + SKAdNetworkIdentifier + 4468km3ulz.skadnetwork + + + SKAdNetworkIdentifier + 2u9pt9hc89.skadnetwork + + + SKAdNetworkIdentifier + 8s468mfl3y.skadnetwork + + + SKAdNetworkIdentifier + klf5c3l5u5.skadnetwork + + + SKAdNetworkIdentifier + ppxm28t8ap.skadnetwork + + + SKAdNetworkIdentifier + ecpz2srf59.skadnetwork + + + SKAdNetworkIdentifier + uw77j35x4d.skadnetwork + + + SKAdNetworkIdentifier + pwa73g5rt2.skadnetwork + + + SKAdNetworkIdentifier + mlmmfzh3r3.skadnetwork + + + SKAdNetworkIdentifier + 578prtvx9j.skadnetwork + + + SKAdNetworkIdentifier + 4dzt52r2t5.skadnetwork + + + SKAdNetworkIdentifier + e5fvkxwrpn.skadnetwork + + + SKAdNetworkIdentifier + 8c4e2ghe7u.skadnetwork + + + SKAdNetworkIdentifier + zq492l623r.skadnetwork + + + SKAdNetworkIdentifier + 3rd42ekr43.skadnetwork + + + SKAdNetworkIdentifier + 3qcr597p9d.skadnetwork + + UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + audio + fetch + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -53,206 +259,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - GADApplicationIdentifier - ca-app-pub-5684307632319406~7113477061 - SKAdNetworkItems - - - SKAdNetworkIdentifier - cstr6suwn9.skadnetwork - - - SKAdNetworkIdentifier - 4fzdc2evr5.skadnetwork - - - SKAdNetworkIdentifier - 4pfyvq9l8r.skadnetwork - - - SKAdNetworkIdentifier - 2fnua5tdw4.skadnetwork - - - SKAdNetworkIdentifier - ydx93a7ass.skadnetwork - - - SKAdNetworkIdentifier - 5a6flpkh64.skadnetwork - - - SKAdNetworkIdentifier - p78axxw29g.skadnetwork - - - SKAdNetworkIdentifier - v72qych5uu.skadnetwork - - - SKAdNetworkIdentifier - ludvb6z3bs.skadnetwork - - - SKAdNetworkIdentifier - cp8zw746q7.skadnetwork - - - SKAdNetworkIdentifier - 3sh42y64q3.skadnetwork - - - SKAdNetworkIdentifier - c6k4g5qg8m.skadnetwork - - - SKAdNetworkIdentifier - s39g8k73mm.skadnetwork - - - SKAdNetworkIdentifier - 3qy4746246.skadnetwork - - - SKAdNetworkIdentifier - f38h382jlk.skadnetwork - - - SKAdNetworkIdentifier - hs6bdukanm.skadnetwork - - - SKAdNetworkIdentifier - v4nxqhlyqp.skadnetwork - - - SKAdNetworkIdentifier - wzmmz9fp6w.skadnetwork - - - SKAdNetworkIdentifier - yclnxrl5pm.skadnetwork - - - SKAdNetworkIdentifier - t38b2kh725.skadnetwork - - - SKAdNetworkIdentifier - 7ug5zh24hu.skadnetwork - - - SKAdNetworkIdentifier - gta9lk7p23.skadnetwork - - - SKAdNetworkIdentifier - vutu7akeur.skadnetwork - - - SKAdNetworkIdentifier - y5ghdn5j9k.skadnetwork - - - SKAdNetworkIdentifier - n6fk4nfna4.skadnetwork - - - SKAdNetworkIdentifier - v9wttpbfk9.skadnetwork - - - SKAdNetworkIdentifier - n38lu8286q.skadnetwork - - - SKAdNetworkIdentifier - 47vhws6wlr.skadnetwork - - - SKAdNetworkIdentifier - kbd757ywx3.skadnetwork - - - SKAdNetworkIdentifier - 9t245vhmpl.skadnetwork - - - SKAdNetworkIdentifier - eh6m2bh4zr.skadnetwork - - - SKAdNetworkIdentifier - a2p9lx4jpn.skadnetwork - - - SKAdNetworkIdentifier - 22mmun2rn5.skadnetwork - - - SKAdNetworkIdentifier - 4468km3ulz.skadnetwork - - - SKAdNetworkIdentifier - 2u9pt9hc89.skadnetwork - - - SKAdNetworkIdentifier - 8s468mfl3y.skadnetwork - - - SKAdNetworkIdentifier - klf5c3l5u5.skadnetwork - - - SKAdNetworkIdentifier - ppxm28t8ap.skadnetwork - - - SKAdNetworkIdentifier - ecpz2srf59.skadnetwork - - - SKAdNetworkIdentifier - uw77j35x4d.skadnetwork - - - SKAdNetworkIdentifier - pwa73g5rt2.skadnetwork - - - SKAdNetworkIdentifier - mlmmfzh3r3.skadnetwork - - - SKAdNetworkIdentifier - 578prtvx9j.skadnetwork - - - SKAdNetworkIdentifier - 4dzt52r2t5.skadnetwork - - - SKAdNetworkIdentifier - e5fvkxwrpn.skadnetwork - - - SKAdNetworkIdentifier - 8c4e2ghe7u.skadnetwork - - - SKAdNetworkIdentifier - zq492l623r.skadnetwork - - - SKAdNetworkIdentifier - 3rd42ekr43.skadnetwork - - - SKAdNetworkIdentifier - 3qcr597p9d.skadnetwork - - - \ No newline at end of file + diff --git a/lib/ads/app_open_ad_manager.dart b/lib/ads/app_open_ad_manager.dart index 90df786..7b480a8 100644 --- a/lib/ads/app_open_ad_manager.dart +++ b/lib/ads/app_open_ad_manager.dart @@ -7,7 +7,10 @@ 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/data/storage/music_box.dart'; +import 'package:tone_snap/modules/launch/launch_controller.dart'; import 'package:tone_snap/utils/log_util.dart'; class AppOpenAdManager { @@ -26,13 +29,16 @@ class AppOpenAdManager { AppOpenAd? _appOpenAd; bool _isShowingAd = false; + /// 记录关闭时的时间,用于下一次展示时计算时间差 + DateTime? closeDateTime; + /// 开屏广告单元id final adUnitId = Platform.isAndroid - ? (kDebugMode ? 'ca-app-pub-3940256099942544/9257395921' : '') - : (kDebugMode ? 'ca-app-pub-3940256099942544/5575463023' : 'ca-app-pub-5684307632319406/2523581084'); + ? (kReleaseMode ? '' : 'ca-app-pub-3940256099942544/9257395921') + : (kReleaseMode ? 'ca-app-pub-5684307632319406/2523581084' : 'ca-app-pub-3940256099942544/5575463023'); /// 加载广告 - void loadAd() async { + Future loadAd() async { final List connectivityResult = await (Connectivity().checkConnectivity()); if (connectivityResult.contains(ConnectivityResult.none)) { return; @@ -40,7 +46,7 @@ class AppOpenAdManager { if (isAdAvailable) { return; } - AppOpenAd.load( + await AppOpenAd.load( adUnitId: adUnitId, request: const AdRequest(), adLoadCallback: AppOpenAdLoadCallback( @@ -48,6 +54,11 @@ class AppOpenAdManager { LogUtil.d('开屏广告加载完成'); _appOpenAd = ad; _appOpenLoadTime = DateTime.now(); + if (AppOpenAdManager().isAdAvailable) { + if (Get.isRegistered()) { + LaunchController.to.editChangeValue(); + } + } }, onAdFailedToLoad: (error) { LogUtil.e('开屏广告加载失败: $error'); @@ -84,6 +95,17 @@ class AppOpenAdManager { if(onTap != null) onTap(); return; } + if (closeDateTime != null) { + // 计算时间差 + Duration timeDifference = DateTime.now().difference(closeDateTime!); + // 获取配置的 openAppEventDuration + int openAppEventDuration = MusicBox().getOpenAppEventDuration(); + // 检查时间差是否小于10秒 + if (timeDifference < Duration(seconds: openAppEventDuration)) { + return; + } + } + // 设置 fullScreenContentCallback 并显示广告 _appOpenAd!.fullScreenContentCallback = FullScreenContentCallback( // 暂停应用程序中的活动或记录广告展示的时间 @@ -108,6 +130,8 @@ class AppOpenAdManager { }, onAdDismissedFullScreenContent: (ad) { LogUtil.d('$ad onAdDismissedFullScreenContent'); + closeDateTime = DateTime.now(); + // 显示状态栏(用户关闭广告后) SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); diff --git a/lib/ads/interstitial_ad_manager.dart b/lib/ads/interstitial_ad_manager.dart index ec4cef3..7a44ec1 100644 --- a/lib/ads/interstitial_ad_manager.dart +++ b/lib/ads/interstitial_ad_manager.dart @@ -27,8 +27,8 @@ class InterstitialAdManager { /// 插页广告单元id final adUnitId = Platform.isAndroid - ? (kDebugMode ? 'ca-app-pub-3940256099942544/1033173712' : '') - : (kDebugMode ? 'ca-app-pub-3940256099942544/4411468910' : 'ca-app-pub-5684307632319406/2760767691'); + ? (kReleaseMode ? '' : 'ca-app-pub-3940256099942544/1033173712') + : (kReleaseMode ? 'ca-app-pub-5684307632319406/2760767691' : 'ca-app-pub-3940256099942544/4411468910'); /// 加载广告 void loadAd() async { diff --git a/lib/components/base_easyloading.dart b/lib/components/base_easyloading.dart index 9609730..52bbfe5 100644 --- a/lib/components/base_easyloading.dart +++ b/lib/components/base_easyloading.dart @@ -43,7 +43,7 @@ class BaseEasyLoading { bool show = true, }) { EasyLoading.instance.userInteractions = false; - if (show) EasyLoading.show(status: value, dismissOnTap: true); + if (show) EasyLoading.show(status: value, dismissOnTap: false); } static void dismiss({bool dismiss = true}) { diff --git a/lib/components/dialog/add_playlist_dialog.dart b/lib/components/dialog/add_playlist_dialog.dart new file mode 100644 index 0000000..8e08457 --- /dev/null +++ b/lib/components/dialog/add_playlist_dialog.dart @@ -0,0 +1,143 @@ +// Author: fengshengxiong +// Date: 2024/5/11 +// Description: 创建播放列表弹框 + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/divider_widget.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class CreatePlaylistDialog extends Dialog { + CreatePlaylistDialog({super.key,this.name, required this.onTap}); + + final _textEditingController = TextEditingController(); + final String? name; + final Function(String name) onTap; + + @override + Widget build(BuildContext context) { + _textEditingController.text = name ?? ''; + return Scaffold( + backgroundColor: Colors.transparent, + body: Material( + type: MaterialType.transparency, + child: Center( + child: IntrinsicHeight( + child: Container( + width: 1.sw * 0.72, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: const Color(0xFF282A2C), + borderRadius: BorderRadius.circular(12.r), + ), + child: Column( + children: [ + SizedBox(height: 19.h), + Text( + 'Create playlist', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 17.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 19.h), + Container( + height: 30.h, + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + color: const Color(0xFF1c1c1e), + borderRadius: BorderRadius.circular(6).r, + border: Border.all( + color: const Color(0xff333437), + width: 1.w, + ), + ), + child: TextField( + maxLines: 1, + controller: _textEditingController, + textInputAction: TextInputAction.done, + keyboardType: TextInputType.text, + style: TextStyle(color: Colors.white, fontSize: 15.sp), + textAlignVertical: TextAlignVertical.center, + maxLength: 30, + decoration: InputDecoration( + counterText: '', + hintText: 'My playlist1', + hintStyle: TextStyle( + color: const Color(0xFF5a635f), + fontSize: 15.sp, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 10).w, + isCollapsed: true, + border: InputBorder.none, + ), + ), + ), + SizedBox(height: 19.h), + DividerWidget( + height: 0.5.h, + color: const Color(0xA6545458), + ), + SizedBox( + height: 44.h, + child: Row( + children: [ + _optionButton('Cancel', false), + Container( + width: 0.5.w, + height: double.infinity, + color: const Color(0xA6545458), + ), + _optionButton('Confirm', true), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _optionButton(String label, bool isConfirm) { + return Expanded( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + if (isConfirm) { + if (ObjUtil.isNotEmpty(_textEditingController.text)) { + onTap(_textEditingController.text); + } else { + onTap('My playlist1'); + } + Get.back(); + } else { + Get.back(); + } + }, + child: SizedBox( + height: double.infinity, + child: Center( + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + color: isConfirm ? seedColor : Colors.white, + fontSize: 17.sp, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/components/dialog/rename_dialog.dart b/lib/components/dialog/rename_dialog.dart index 0e481da..8056406 100644 --- a/lib/components/dialog/rename_dialog.dart +++ b/lib/components/dialog/rename_dialog.dart @@ -134,7 +134,7 @@ class RenameDialogState extends State { child: InkWell( onTap: () { if (isConfirm) { - if (!ObjUtil.isNotEmptyStr(_textEditingController.text)) { + if (!ObjUtil.isNotEmpty(_textEditingController.text)) { BaseEasyLoading.toast('name cannot be empty'); return; } diff --git a/lib/components/get_bind_widget.dart b/lib/components/get_bind_widget.dart new file mode 100644 index 0000000..d3c0bde --- /dev/null +++ b/lib/components/get_bind_widget.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +/// [author] Fson +/// [date] 2022/12/7 +/// [description] + +/// GetBindWidget can bind GetxController, and when the page is disposed, +/// it can automatically destroy the bound related GetXController +/// +/// +/// Sample: +/// +/// class SampleController extends GetxController { +/// final String title = 'My Awesome View'; +/// } +/// +/// class SamplePage extends StatelessWidget { +/// final controller = SampleController(); +/// +/// @override +/// Widget build(BuildContext context) { +/// return GetBindWidget( +/// bind: controller, +/// child: Container(), +/// ); +/// } +/// } +class GetBindWidget extends StatefulWidget { + const GetBindWidget({ + super.key, + this.bind, + this.tag, + this.binds, + this.tags, + required this.child, + }) : assert( + binds == null || tags == null || binds.length == tags.length, + 'The binds and tags arrays length should be equal\n' + 'and the elements in the two arrays correspond one-to-one', + ); + + final GetxController? bind; + final String? tag; + + final List? binds; + final List? tags; + + final Widget child; + + @override + GetBindWidgetState createState() => GetBindWidgetState(); +} + +class GetBindWidgetState extends State{ + @override + Widget build(BuildContext context) { + return widget.child; + } + + @override + void dispose() { + _closeGetXController(); + _closeGetXControllers(); + + super.dispose(); + } + + ///Close GetxController bound to the current page + void _closeGetXController() { + if (widget.bind == null) { + return; + } + + var key = widget.bind.runtimeType.toString() + (widget.tag ?? ''); + GetInstance().delete(key: key); + } + + ///Batch close GetxController bound to the current page + void _closeGetXControllers() { + if (widget.binds == null) { + return; + } + + for (var i = 0; i < widget.binds!.length; i++) { + var type = widget.binds![i].runtimeType.toString(); + + if (widget.tags == null) { + GetInstance().delete(key: type); + } else { + var key = type + (widget.tags?[i] ?? ''); + GetInstance().delete(key: key); + } + } + } +} diff --git a/lib/components/music_bar/music_bar_controller.dart b/lib/components/music_bar/music_bar_controller.dart deleted file mode 100644 index 90f90ba..0000000 --- a/lib/components/music_bar/music_bar_controller.dart +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 662fff5..0000000 --- a/lib/components/music_bar/music_bar_view.dart +++ /dev/null @@ -1,95 +0,0 @@ -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 57df683..09a39fa 100644 --- a/lib/components/my_marquee_text.dart +++ b/lib/components/my_marquee_text.dart @@ -20,8 +20,8 @@ class MyMarqueeText extends StatelessWidget { @override Widget build(BuildContext context) { return Marquee( - delay: const Duration(seconds: 2), - duration: Duration(seconds: enable ? 12 : 0), + delay: Duration.zero, + duration: Duration(seconds: enable ? 16 : 0), pause: Duration.zero, gap: 80, child: Text(text, style: textStyle), diff --git a/lib/components/network_image_widget.dart b/lib/components/network_image_widget.dart index 32b7a73..82bde1f 100644 --- a/lib/components/network_image_widget.dart +++ b/lib/components/network_image_widget.dart @@ -4,6 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:tone_snap/generated/assets.dart'; class NetworkImageWidget extends StatelessWidget { @@ -14,7 +15,9 @@ class NetworkImageWidget extends StatelessWidget { this.radius = 0.0, this.url, this.fit = BoxFit.cover, - this.placeholder, + this.placeholderWidth, + this.placeholderHeight, + this.placeholderImg, this.noPlaceholder = false, }); @@ -23,7 +26,9 @@ class NetworkImageWidget extends StatelessWidget { final double radius; final String? url; final BoxFit fit; - final String? placeholder; + final double? placeholderWidth; + final double? placeholderHeight; + final String? placeholderImg; final bool noPlaceholder; @override @@ -35,20 +40,27 @@ class NetworkImageWidget extends StatelessWidget { height: height, imageUrl: '$url', fit: fit, - placeholder: noPlaceholder ? null : (context, url) { - return _placeholderWidget(Assets.sideBImgPlaceholder); + placeholder: (context, url) { + return noPlaceholder ? Container() : _placeholderWidget(); }, - errorWidget: noPlaceholder ? null : (context, url, error) { - return _placeholderWidget(Assets.sideBImgError); + errorWidget: (context, url, error) { + return noPlaceholder ? Container() : _placeholderWidget(); }, ), ); } - Widget _placeholderWidget(String img) { - return Image.asset( - placeholder ?? img, - color: Colors.white12, + Widget _placeholderWidget() { + return Container( + color: const Color(0xFF242529), + child: FittedBox( + fit: BoxFit.none, + child: Image.asset( + placeholderImg ?? Assets.sideBMusicPlaceholder, + width: placeholderWidth ?? 24.w, + height: placeholderHeight ?? 24.w, + ), + ), ); } } diff --git a/lib/components/refresh/base_easyrefresh.dart b/lib/components/refresh/base_easyrefresh.dart index dce1782..d04ec95 100644 --- a/lib/components/refresh/base_easyrefresh.dart +++ b/lib/components/refresh/base_easyrefresh.dart @@ -1,58 +1,42 @@ -// Author: fengshengxiong -// Date: 2024/5/7 -// Description: 下拉刷新、上拉加载 - import 'dart:async'; + import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:tone_snap/components/view_state_widget.dart'; class BaseEasyRefresh extends StatelessWidget { - const BaseEasyRefresh({ - super.key, - required this.controller, - this.header, - this.footer, - this.onRefresh, - this.onLoad, - this.refreshOnStart = false, - required this.viewState, - required this.child, - this.width, - this.height, - }); - - final EasyRefreshController controller; + final EasyRefreshController? controller; + final bool noMoreRefresh; + final bool noMoreLoad; + final bool refreshOnStart; final Header? header; final Footer? footer; final FutureOr Function()? onRefresh; final FutureOr Function()? onLoad; - final bool refreshOnStart; - final ViewState viewState; final Widget child; - final double? width; - final double? height; + + const BaseEasyRefresh({ + super.key, + this.controller, + this.noMoreRefresh = false, + this.noMoreLoad = false, + this.refreshOnStart = false, + this.header, + this.footer, + required this.child, + this.onRefresh, + this.onLoad, + }); @override Widget build(BuildContext context) { return EasyRefresh( + refreshOnStart: refreshOnStart, controller: controller, - header: header ?? const ClassicHeader(), - footer: footer ?? const ClassicFooter(), + header: header, + footer: footer, onRefresh: onRefresh, onLoad: onLoad, - refreshOnStart: refreshOnStart, - child: viewState == ViewState.normal ? child : SingleChildScrollView( - child: SizedBox( - width: width ?? 1.sw, - height: height ?? 300.h, - child: ViewStateWidget( - viewState: viewState, - child: child, - ), - ), - ), + child: child, ); } } diff --git a/lib/components/view_state_widget.dart b/lib/components/view_state_widget.dart index afb0a53..3cf4775 100644 --- a/lib/components/view_state_widget.dart +++ b/lib/components/view_state_widget.dart @@ -4,6 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:tone_snap/data/enum/app_side_enum.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/global/app_config.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; /// 四种视图状态 enum ViewState { normal, error, loading, empty } @@ -26,11 +30,9 @@ class ViewStateWidget extends StatelessWidget { case ViewState.normal: return child; case ViewState.loading: - return loadingView( - backgroundColor: cpiBgColor, - ); + return loadingView(backgroundColor: cpiBgColor); case ViewState.empty: - return emptyView(); + return AppConfig.appSideEnum == AppSideEnum.sideA ? emptyViewA() : emptyViewB(); case ViewState.error: return errorView(); } @@ -40,23 +42,19 @@ class ViewStateWidget extends StatelessWidget { /// 加载中视图 Widget loadingView({ Color? color, - Animation? valueColor, Color? backgroundColor, - double? value, }) { return Center( child: CircularProgressIndicator( strokeWidth: 3, - color: color, - valueColor: valueColor, + color: AppConfig.appSideEnum == AppSideEnum.sideA ? color : seedColor, backgroundColor: backgroundColor, - value: value, ), ); } /// 空视图 -Widget emptyView({String? msg, Color? textColor}) { +Widget emptyViewA({String? msg, Color? textColor}) { return Center( child: Text( msg ?? 'No data', @@ -69,6 +67,17 @@ Widget emptyView({String? msg, Color? textColor}) { ); } +/// 空视图2 +Widget emptyViewB() { + return Center( + child: Image.asset( + Assets.sideBEmpty, + width: 180.w, + height: 160.h, + ), + ); +} + /// 错误视图 Widget errorView({String? msg, Color? textColor}) { return Center( diff --git a/lib/data/api/music_api.dart b/lib/data/api/music_api.dart index 672fa8c..34c9be4 100644 --- a/lib/data/api/music_api.dart +++ b/lib/data/api/music_api.dart @@ -2,65 +2,72 @@ // Date: 2024/6/18 // Description: 音乐播放器Api -import 'package:devicelocale/devicelocale.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:dio/dio.dart'; +import 'package:flutter/material.dart'; import 'package:tone_snap/data/models/next_model.dart'; -import 'package:tone_snap/utils/date_util.dart'; +import 'package:tone_snap/data/models/player_model.dart'; +import 'package:tone_snap/data/models/search_suggestions_model.dart'; +import 'package:tone_snap/data/network/base_error.dart'; +import 'package:tone_snap/data/network/dio_client.dart'; +import 'package:tone_snap/data/storage/music_box.dart'; +import 'package:tone_snap/global/app_config.dart'; class MusicApi { static const String baseUrl = 'https://music.youtube.com/youtubei/v1/'; - /// 首页browse接口 + /// browse static Future browse({ String? visitorData, Map? queryParameters, T Function(Map)? formJson, + bool showLoading = false, + bool showToast = false, }) async { - String date = DateUtil.getSevenDaysAgo(); - String locale = await Devicelocale.currentLocale ?? AppConfig.defaultLocale; + String clientVersion = MusicBox().getClientVersion(); + String preferredLanguages = WidgetsBinding.instance.platformDispatcher.locale.languageCode; final body = { "context": { "client": { "visitorData": visitorData, "clientName": "WEB_REMIX", - // "clientVersion": "1.$date.01.00", - "clientVersion": "1.20240607.01.00", + "clientVersion": clientVersion, "platform": "DESKTOP", - "hl": locale, - // "gl": AppConfig.isoCode - "gl": 'HK' + "hl": preferredLanguages, + "gl": AppConfig.isoCode } } }; - T? resultModel; + T? model; await DioClient().request( 'browse', + showLoading: showLoading, + showToast: showToast, requestMethod: RequestMethod.post, data: body, queryParameters: queryParameters, formJson: formJson, - success: (model) => resultModel = model, + success: (data) => model = data, ); - return resultModel; + return model; } - /// next接口 - static Future next({String? visitorData, String? playlistId, String? videoId}) async { - String date = DateUtil.getSevenDaysAgo(); - String locale = await Devicelocale.currentLocale ?? AppConfig.defaultLocale; + /// next + static Future next({ + String? playlistId, + String? videoId, + bool showLoading = false, + bool showToast = true, + }) async { + String clientVersion = MusicBox().getClientVersion(); + String preferredLanguages = WidgetsBinding.instance.platformDispatcher.locale.languageCode; final body = { "context": { "client": { - "visitorData": visitorData, "clientName": "WEB_REMIX", - "clientVersion": "1.$date", + "clientVersion": clientVersion, "platform": "DESKTOP", - "hl": locale, - // "gl": AppConfig.isoCode - "gl": 'HK' + "hl": preferredLanguages, + "gl": AppConfig.isoCode } } }; @@ -69,28 +76,34 @@ class MusicApi { 'playlistId': playlistId, 'videoId': videoId }; - NextModel? nextModel; + NextModel? model; await DioClient().request( 'next', - showLoading: true, - showToast: true, + showLoading: showLoading, + showToast: showToast, requestMethod: RequestMethod.post, data: body, queryParameters: queryParameters, formJson: NextModel.fromMap, - success: (model) => nextModel = model, + success: (data) => model = data, ); - return nextModel; + return model; } - /// player接口 - static Future player({String? visitorData, String? videoId}) async { - String date = DateUtil.getSevenDaysAgo(); + /// player + static Future player({ + String? videoId, + CancelToken? cancelToken, + Function(BaseError baseError)? fail, + bool showLoading = false, + bool showToast = true, + }) async { + String playerVersion = MusicBox().getPlayerVersion(); final body = { "context": { "client": { "clientName": "ANDROID_MUSIC", - "clientVersion": "6.07.1", + "clientVersion": playerVersion, "platform": "MOBILE", "browserVersion":"125.0.0.0" } @@ -101,17 +114,92 @@ class MusicApi { 'playlistId': null, 'videoId': videoId }; - PlayerModel? playerModel; + PlayerModel? model; await DioClient().request( 'player', - showLoading: true, - showToast: true, + showLoading: showLoading, + showToast: showToast, requestMethod: RequestMethod.post, data: body, queryParameters: queryParameters, + cancelToken: cancelToken, formJson: PlayerModel.fromMap, - success: (model) => playerModel = model, + success: (data) => model = data, + fail: fail, ); - return playerModel; + return model; + } + + /// 搜索建议 + static Future searchSuggestions({ + String? input, + CancelToken? cancelToken, + bool showLoading = false, + bool showToast = false, + }) async { + String clientVersion = MusicBox().getClientVersion(); + String preferredLanguages = WidgetsBinding.instance.platformDispatcher.locale.languageCode; + final body = { + "context": { + "client": { + "clientName": "WEB_REMIX", + "clientVersion": clientVersion, + "platform": "DESKTOP", + "hl": preferredLanguages, + "gl": AppConfig.isoCode + } + } + }; + Map? queryParameters = { + 'prettyPrint': false, + 'input': input, + }; + SearchSuggestionsModel? model; + await DioClient().request( + 'music/get_search_suggestions', + showLoading: showLoading, + showToast: showToast, + requestMethod: RequestMethod.post, + data: body, + queryParameters: queryParameters, + cancelToken: cancelToken, + formJson: SearchSuggestionsModel.fromMap, + success: (data) => model = data, + ); + return model; + } + + /// 搜索 + static Future search({ + Map? queryParameters, + T Function(Map)? formJson, + bool showLoading = false, + bool showToast = true, + }) async { + String clientVersion = MusicBox().getClientVersion(); + String preferredLanguages = WidgetsBinding.instance.platformDispatcher.locale.languageCode; + final body = { + "context": { + "client": { + "clientName": "WEB_REMIX", + "clientVersion": clientVersion, + "platform": "DESKTOP", + "hl": preferredLanguages, + "gl": AppConfig.isoCode + } + } + }; + T? model; + await DioClient().request( + 'search', + showLoading: showLoading, + showToast: showToast, + requestMethod: RequestMethod.post, + data: body, + queryParameters: queryParameters, + formJson: formJson, + success: (data) => model = data, + ); + return model; } } diff --git a/lib/data/api/tikustok_api.dart b/lib/data/api/tikustok_api.dart index 134f599..56259ed 100644 --- a/lib/data/api/tikustok_api.dart +++ b/lib/data/api/tikustok_api.dart @@ -9,13 +9,13 @@ import 'package:tone_snap/data/models/isocode_model.dart'; class TikUsTokApi { static const String baseUrl = 'https://api.tikustok.com/'; - /// 获取所在区域、ip - static Future?> getIp() async { - BaseModel? baseModel; - await DioClient(baseUrl: baseUrl).request>( + /// 获取所在区域 + static Future?> getIsoCode() async { + BaseModel? baseModel; + await DioClient(baseUrl: baseUrl).request>( 'app/common/getIPInfo', requestMethod: RequestMethod.get, - formJson: (json) => BaseModel.fromMap(json, IosCodeModel.fromMap), + formJson: (json) => BaseModel.fromMap(json, IsoCodeModel.fromMap), success: (model) => baseModel = model, ); return baseModel; diff --git a/lib/data/enum/browse_type.dart b/lib/data/enum/music_type.dart similarity index 53% rename from lib/data/enum/browse_type.dart rename to lib/data/enum/music_type.dart index b8ac3f1..c1c56d5 100644 --- a/lib/data/enum/browse_type.dart +++ b/lib/data/enum/music_type.dart @@ -1,9 +1,9 @@ // Author: fengshengxiong // Date: 2024/6/13 -// Description: 资源类型 +// Description: 音乐类型 -enum BrowseType { - /// 电台/单曲 +enum MusicType { + /// 单曲 musicVideoTypeAtv(name: 'MUSIC_VIDEO_TYPE_ATV'), /// 视频 @@ -12,11 +12,11 @@ enum BrowseType { /// 专辑 musicPageTypeAlbum(name: 'MUSIC_PAGE_TYPE_ALBUM'), - /// 艺术家 - // musicPageTypeArtist(name: 'MUSIC_PAGE_TYPE_ARTIST'), - /// 歌单/列表 - musicPageTypePlaylist(name: 'MUSIC_PAGE_TYPE_PLAYLIST'); + musicPageTypePlaylist(name: 'MUSIC_PAGE_TYPE_PLAYLIST'), + + /// 艺术家 + musicPageTypeArtist(name: 'MUSIC_PAGE_TYPE_ARTIST'); /// 歌词 // musicPageTypeTrackLyrics(name: 'MUSIC_PAGE_TYPE_TRACK_LYRICS'), @@ -24,20 +24,21 @@ enum BrowseType { /// 相关内容 // musicPageTypeTrackRelated(name: 'MUSIC_PAGE_TYPE_TRACK_RELATED'); - const BrowseType({ + const MusicType({ required this.name, }); final String name; } -extension BrowseTypeExtension on BrowseType { +extension MusicTypeExtension on MusicType { static bool isThereAny(String? type) { - if (type == BrowseType.musicVideoTypeAtv.name || - type == BrowseType.musicVideoTypeOmv.name || - type == BrowseType.musicPageTypeAlbum.name || - // type == BrowseType.musicPageTypeArtist.name || - type == BrowseType.musicPageTypePlaylist.name) { + if (type == MusicType.musicVideoTypeAtv.name || + // type == BrowseType.musicVideoTypeOmv.name || + type == MusicType.musicPageTypeAlbum.name || + type == MusicType.musicPageTypePlaylist.name + // type == BrowseType.musicPageTypeArtist.name + ) { return true; } else { return false; diff --git a/lib/data/models/browse_group_model.dart b/lib/data/models/browse_group_model.dart new file mode 100644 index 0000000..ab2ab43 --- /dev/null +++ b/lib/data/models/browse_group_model.dart @@ -0,0 +1,35 @@ +// Author: fengshengxiong +// Date: 2024/6/20 +// Description: Browse分组模型 + +import 'dart:convert'; + +import 'package:tone_snap/data/models/music_model.dart'; + +class BrowseGroupModel { + String? groupTitle; + String? musicType; + List? browseList; + + BrowseGroupModel({ + this.groupTitle, + this.musicType, + this.browseList, + }); + + factory BrowseGroupModel.fromJson(String str) => BrowseGroupModel.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory BrowseGroupModel.fromMap(Map json) => BrowseGroupModel( + groupTitle: json["groupTitle"], + musicType: json["musicType"], + browseList: json["browseList"] == null ? [] : List.from(json["browseList"]!.map((x) => MusicModel.fromMap(x))), + ); + + Map toMap() => { + "groupTitle": groupTitle, + "musicType": musicType, + "browseList": browseList == null ? [] : List.from(browseList!.map((x) => x.toMap())), + }; +} diff --git a/lib/data/models/browse_model.dart b/lib/data/models/browse_model.dart index cbae2e8..3d14b81 100644 --- a/lib/data/models/browse_model.dart +++ b/lib/data/models/browse_model.dart @@ -9,6 +9,7 @@ class BrowseModel { Contents? contents; ContinuationContents? continuationContents; String? trackingParams; + Microformat? microformat; int? maxAgeStoreSeconds; ThumbnailClass? background; @@ -146,9 +147,11 @@ class ThumbnailElement { class Contents { SingleColumnBrowseResultsRenderer? singleColumnBrowseResultsRenderer; + TwoColumnBrowseResultsRenderer? twoColumnBrowseResultsRenderer; Contents({ this.singleColumnBrowseResultsRenderer, + this.twoColumnBrowseResultsRenderer, }); factory Contents.fromJson(String str) => Contents.fromMap(json.decode(str)); @@ -157,10 +160,12 @@ class Contents { factory Contents.fromMap(Map json) => Contents( singleColumnBrowseResultsRenderer: json["singleColumnBrowseResultsRenderer"] == null ? null : SingleColumnBrowseResultsRenderer.fromMap(json["singleColumnBrowseResultsRenderer"]), + twoColumnBrowseResultsRenderer: json["twoColumnBrowseResultsRenderer"] == null ? null : TwoColumnBrowseResultsRenderer.fromMap(json["twoColumnBrowseResultsRenderer"]), ); Map toMap() => { "singleColumnBrowseResultsRenderer": singleColumnBrowseResultsRenderer?.toMap(), + "twoColumnBrowseResultsRenderer": twoColumnBrowseResultsRenderer?.toMap(), }; } @@ -303,10 +308,12 @@ class SectionList { class SectionListContinuationContent { MusicCarouselShelfRenderer? musicCarouselShelfRenderer; MusicTastebuilderShelfRenderer? musicTastebuilderShelfRenderer; + MusicResponsiveHeaderRenderer? musicResponsiveHeaderRenderer; SectionListContinuationContent({ this.musicCarouselShelfRenderer, this.musicTastebuilderShelfRenderer, + this.musicResponsiveHeaderRenderer, }); factory SectionListContinuationContent.fromJson(String str) => SectionListContinuationContent.fromMap(json.decode(str)); @@ -316,11 +323,13 @@ class SectionListContinuationContent { factory SectionListContinuationContent.fromMap(Map json) => SectionListContinuationContent( musicCarouselShelfRenderer: json["musicCarouselShelfRenderer"] == null ? null : MusicCarouselShelfRenderer.fromMap(json["musicCarouselShelfRenderer"]), musicTastebuilderShelfRenderer: json["musicTastebuilderShelfRenderer"] == null ? null : MusicTastebuilderShelfRenderer.fromMap(json["musicTastebuilderShelfRenderer"]), + musicResponsiveHeaderRenderer: json["musicResponsiveHeaderRenderer"] == null ? null : MusicResponsiveHeaderRenderer.fromMap(json["musicResponsiveHeaderRenderer"]), ); Map toMap() => { "musicCarouselShelfRenderer": musicCarouselShelfRenderer?.toMap(), "musicTastebuilderShelfRenderer": musicTastebuilderShelfRenderer?.toMap(), + "musicResponsiveHeaderRenderer": musicResponsiveHeaderRenderer?.toMap(), }; } @@ -389,6 +398,7 @@ class MusicResponsiveListItemRenderer { ThumbnailClass? thumbnail; Overlay? overlay; List? flexColumns; + List? fixedColumns; MusicResponsiveListItemRendererMenu? menu; PlaylistItemData? playlistItemData; String? flexColumnDisplayStyle; @@ -399,6 +409,7 @@ class MusicResponsiveListItemRenderer { this.thumbnail, this.overlay, this.flexColumns, + this.fixedColumns, this.menu, this.playlistItemData, this.flexColumnDisplayStyle, @@ -414,6 +425,7 @@ class MusicResponsiveListItemRenderer { thumbnail: json["thumbnail"] == null ? null : ThumbnailClass.fromMap(json["thumbnail"]), 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 : MusicResponsiveListItemRendererMenu.fromMap(json["menu"]), playlistItemData: json["playlistItemData"] == null ? null : PlaylistItemData.fromMap(json["playlistItemData"]), flexColumnDisplayStyle: json["flexColumnDisplayStyle"], @@ -425,6 +437,7 @@ class MusicResponsiveListItemRenderer { "thumbnail": thumbnail?.toMap(), "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(), "flexColumnDisplayStyle": flexColumnDisplayStyle, @@ -1706,9 +1719,13 @@ class VerticalGradient { class PurpleContent { PurpleMusicPlayButtonRenderer? musicPlayButtonRenderer; + MusicShelfRenderer? musicShelfRenderer; + MusicPlaylistShelfRenderer? musicPlaylistShelfRenderer; PurpleContent({ this.musicPlayButtonRenderer, + this.musicShelfRenderer, + this.musicPlaylistShelfRenderer, }); factory PurpleContent.fromJson(String str) => PurpleContent.fromMap(json.decode(str)); @@ -1717,10 +1734,14 @@ class PurpleContent { factory PurpleContent.fromMap(Map json) => PurpleContent( musicPlayButtonRenderer: json["musicPlayButtonRenderer"] == null ? null : PurpleMusicPlayButtonRenderer.fromMap(json["musicPlayButtonRenderer"]), + musicShelfRenderer: json["musicShelfRenderer"] == null ? null : MusicShelfRenderer.fromMap(json["musicShelfRenderer"]), + musicPlaylistShelfRenderer: json["musicPlaylistShelfRenderer"] == null ? null : MusicPlaylistShelfRenderer.fromMap(json["musicPlaylistShelfRenderer"]), ); Map toMap() => { "musicPlayButtonRenderer": musicPlayButtonRenderer?.toMap(), + "musicShelfRenderer": musicShelfRenderer?.toMap(), + "musicPlaylistShelfRenderer": musicPlaylistShelfRenderer?.toMap(), }; } @@ -3027,3 +3048,1419 @@ class Param { "value": value, }; } + +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 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 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 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 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 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 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 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 MusicItemThumbnailOverlayRenderer { + Background? 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 : Background.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 Background { + VerticalGradient? verticalGradient; + + Background({ + this.verticalGradient, + }); + + factory Background.fromJson(String str) => Background.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Background.fromMap(Map json) => Background( + verticalGradient: json["verticalGradient"] == null ? null : VerticalGradient.fromMap(json["verticalGradient"]), + ); + + Map toMap() => { + "verticalGradient": verticalGradient?.toMap(), + }; +} + +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 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 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 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 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 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 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 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 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 MusicPlaylistShelfRenderer { + String? playlistId; + List? contents; + int? collapsedItemCount; + String? trackingParams; + bool? contentsMultiSelectable; + + MusicPlaylistShelfRenderer({ + this.playlistId, + this.contents, + this.collapsedItemCount, + this.trackingParams, + this.contentsMultiSelectable, + }); + + factory MusicPlaylistShelfRenderer.fromJson(String str) => MusicPlaylistShelfRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicPlaylistShelfRenderer.fromMap(Map json) => MusicPlaylistShelfRenderer( + playlistId: json["playlistId"], + contents: json["contents"] == null ? [] : List.from(json["contents"]!.map((x) => MusicShelfRendererContent.fromMap(x))), + collapsedItemCount: json["collapsedItemCount"], + trackingParams: json["trackingParams"], + contentsMultiSelectable: json["contentsMultiSelectable"], + ); + + Map toMap() => { + "playlistId": playlistId, + "contents": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), + "collapsedItemCount": collapsedItemCount, + "trackingParams": trackingParams, + "contentsMultiSelectable": contentsMultiSelectable, + }; +} \ No newline at end of file diff --git a/lib/data/models/home_model.dart b/lib/data/models/home_model.dart deleted file mode 100644 index cd8ecde..0000000 --- a/lib/data/models/home_model.dart +++ /dev/null @@ -1,77 +0,0 @@ -// Author: fengshengxiong -// Date: 2024/6/20 -// Description: 首页实际展示数据模型 - -import 'dart:convert'; - -class HomeModel { - String? headerTitle; - List? contents; - String? browseType; - - HomeModel({ - this.headerTitle, - this.contents, - this.browseType, - }); - - factory HomeModel.fromJson(String str) => HomeModel.fromMap(json.decode(str)); - - String toJson() => json.encode(toMap()); - - factory HomeModel.fromMap(Map json) => HomeModel( - headerTitle: json["headerTitle"], - contents: json["contents"] == null ? [] : List.from(json["contents"]!.map((x) => Content.fromMap(x))), - browseType: json["browseType"] - ); - - Map toMap() => { - "headerTitle": headerTitle, - "contents": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), - "browseType": browseType, - }; -} - -class Content { - String? title; - String? subTitle; - String? thumbnail; - String? videoId; - String? playlistId; - String? browseId; - String? params; - - Content({ - this.title, - this.subTitle, - this.thumbnail, - this.videoId, - this.playlistId, - this.browseId, - this.params, - }); - - factory Content.fromJson(String str) => Content.fromMap(json.decode(str)); - - String toJson() => json.encode(toMap()); - - factory Content.fromMap(Map json) => Content( - title: json["title"], - subTitle: json["subTitle"], - thumbnail: json["thumbnail"], - videoId: json["videoId"], - playlistId: json["playlistId"], - browseId: json["browseId"], - params: json["params"], - ); - - Map toMap() => { - "title": title, - "subTitle": subTitle, - "thumbnail": thumbnail, - "videoId": videoId, - "playlistId": playlistId, - "browseId": browseId, - "params": params, - }; -} diff --git a/lib/data/models/isocode_model.dart b/lib/data/models/isocode_model.dart index 4fcd251..66f853e 100644 --- a/lib/data/models/isocode_model.dart +++ b/lib/data/models/isocode_model.dart @@ -4,20 +4,20 @@ import 'dart:convert'; -class IosCodeModel { +class IsoCodeModel { String? isoCode; String? ip; - IosCodeModel({ + IsoCodeModel({ this.isoCode, this.ip, }); - factory IosCodeModel.fromJson(String str) => IosCodeModel.fromMap(json.decode(str)); + factory IsoCodeModel.fromJson(String str) => IsoCodeModel.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory IosCodeModel.fromMap(Map json) => IosCodeModel( + factory IsoCodeModel.fromMap(Map json) => IsoCodeModel( isoCode: json["isoCode"], ip: json["ip"], ); diff --git a/lib/data/models/music_model.dart b/lib/data/models/music_model.dart index e3262a9..c43673c 100644 --- a/lib/data/models/music_model.dart +++ b/lib/data/models/music_model.dart @@ -1,9 +1,11 @@ // Author: fengshengxiong // Date: 2024/6/24 -// Description: 音乐模型 +// Description: 音乐个体模型 import 'dart:convert'; +import 'package:background_downloader/background_downloader.dart'; +import 'package:dio/dio.dart'; import 'package:hive/hive.dart'; part 'music_model.g.dart'; @@ -11,42 +13,82 @@ 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(1) + String? title; + @HiveField(2) + String? subtitle; + @HiveField(3) + String? coverUrl; @HiveField(4) - String? playlistId; - @HiveField(5) String? url; + @HiveField(5) + String? localPath; + @HiveField(6) + String? musicType; + @HiveField(7) + String? playlistId; + @HiveField(8) + String? browseId; + @HiveField(9) + String? params; + + /// 收藏状态 + bool isLove = false; + + /// 下载进度、任务状态 + double progress; + TaskStatus? taskStatus; + CancelToken? cancelToken; MusicModel({ - this.title, - this.subTitle, - this.thumbnail, this.videoId, - this.playlistId, + this.title, + this.subtitle, + this.coverUrl, this.url, + this.localPath, + this.musicType, + this.playlistId, + this.browseId, + this.params, + this.isLove = false, + this.progress = 0.0, + this.taskStatus, + this.cancelToken, }); MusicModel copyWith({ - String? title, - String? subTitle, - String? thumbnail, - String? url, String? videoId, + String? title, + String? subtitle, + String? coverUrl, + String? url, + String? localPath, + String? musicType, String? playlistId, + String? browseId, + String? params, + bool? isLove, + double? progress, + TaskStatus? taskStatus, + CancelToken? cancelToken, }) => MusicModel( - title: title ?? this.title, - subTitle: subTitle ?? this.subTitle, - thumbnail: thumbnail ?? this.thumbnail, - url: url ?? this.url, videoId: videoId ?? this.videoId, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + coverUrl: coverUrl ?? this.coverUrl, + url: url ?? this.url, + localPath: localPath ?? this.localPath, + musicType: musicType ?? this.musicType, playlistId: playlistId ?? this.playlistId, + browseId: browseId ?? this.browseId, + params: params ?? this.params, + isLove: isLove ?? this.isLove, + progress: progress ?? this.progress, + taskStatus: taskStatus ?? this.taskStatus, + cancelToken: cancelToken ?? this.cancelToken, ); factory MusicModel.fromJson(String str) => @@ -55,20 +97,36 @@ class MusicModel extends HiveObject { 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"], + title: json["title"], + subtitle: json["subtitle"], + coverUrl: json["coverUrl"], url: json["url"], + localPath: json["localPath"], + musicType: json["musicType"], + playlistId: json["playlistId"], + browseId: json["browseId"], + params: json["params"], + isLove: json["isLove"] ?? false, + progress: json["progress"] ?? 0.0, + taskStatus: json["taskStatus"], + cancelToken: json["cancelToken"], ); Map toMap() => { - "title": title, - "subTitle": subTitle, - "thumbnail": thumbnail, "videoId": videoId, - "playlistId": playlistId, + "title": title, + "subTitle": subtitle, + "coverUrl": coverUrl, "url": url, + "localPath": localPath, + "musicType": musicType, + "playlistId": playlistId, + "browseId": browseId, + "params": params, + "isLove": isLove, + "progress": progress, + "taskStatus": taskStatus, + "cancelToken": cancelToken, }; } diff --git a/lib/data/models/music_model.g.dart b/lib/data/models/music_model.g.dart index 4aa388f..26fdb33 100644 --- a/lib/data/models/music_model.g.dart +++ b/lib/data/models/music_model.g.dart @@ -17,31 +17,40 @@ class MusicModelAdapter extends TypeAdapter { 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?, + videoId: fields[0] as String?, + title: fields[1] as String?, + subtitle: fields[2] as String?, + coverUrl: fields[3] as String?, + url: fields[4] as String?, + localPath: fields[5] as String?, + musicType: fields[6] as String?, + playlistId: fields[7] as String?, + browseId: fields[8] as String?, ); } @override void write(BinaryWriter writer, MusicModel obj) { writer - ..writeByte(6) + ..writeByte(9) ..writeByte(0) - ..write(obj.title) - ..writeByte(1) - ..write(obj.subTitle) - ..writeByte(2) - ..write(obj.thumbnail) - ..writeByte(3) ..write(obj.videoId) + ..writeByte(1) + ..write(obj.title) + ..writeByte(2) + ..write(obj.subtitle) + ..writeByte(3) + ..write(obj.coverUrl) ..writeByte(4) - ..write(obj.playlistId) + ..write(obj.url) ..writeByte(5) - ..write(obj.url); + ..write(obj.localPath) + ..writeByte(6) + ..write(obj.musicType) + ..writeByte(7) + ..write(obj.playlistId) + ..writeByte(8) + ..write(obj.browseId); } @override diff --git a/lib/data/models/playlist_model.dart b/lib/data/models/playlist_model.dart new file mode 100644 index 0000000..891f302 --- /dev/null +++ b/lib/data/models/playlist_model.dart @@ -0,0 +1,85 @@ +// Author: fengshengxiong +// Date: 2024/6/24 +// Description: 播放列表模型 + +import 'dart:convert'; + +import 'package:hive/hive.dart'; +import 'package:tone_snap/data/models/music_model.dart'; + +part 'playlist_model.g.dart'; + +@HiveType(typeId: 3) +class PlaylistModel extends HiveObject { + @HiveField(0) + String id; + @HiveField(1) + String title; + + /// playlists + @HiveField(2) + int? milliseconds; + @HiveField(3) + List? musicList; + + /// collect_playlists + @HiveField(4) + String? params; + @HiveField(5) + String? coverUrl; + @HiveField(6) + String? subtitle; + + PlaylistModel({ + required this.id, + required this.title, + this.milliseconds, + this.musicList, + this.params, + this.coverUrl, + this.subtitle, + }); + + PlaylistModel copyWith({ + required String id, + required String title, + int? milliseconds, + List? musicList, + String? params, + String? coverUrl, + String? subtitle, + }) => + PlaylistModel( + id: id, + title: title, + milliseconds: milliseconds ?? this.milliseconds, + musicList: musicList ?? this.musicList, + params: params ?? this.params, + coverUrl: coverUrl ?? this.coverUrl, + subtitle: subtitle ?? this.subtitle, + ); + + factory PlaylistModel.fromJson(String str) => PlaylistModel.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PlaylistModel.fromMap(Map json) => PlaylistModel( + id: json["id"], + title: json["title"], + milliseconds: json["milliseconds"], + musicList: json["musicList"], + params: json["params"], + coverUrl: json["coverUrl"], + subtitle: json["subtitle"], + ); + + Map toMap() => { + "id": id, + "title": title, + "milliseconds": milliseconds, + "musicList": musicList, + "params": params, + "coverUrl": coverUrl, + "subtitle": subtitle, + }; +} diff --git a/lib/data/models/playlist_model.g.dart b/lib/data/models/playlist_model.g.dart new file mode 100644 index 0000000..2ffabd1 --- /dev/null +++ b/lib/data/models/playlist_model.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'playlist_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class PlaylistModelAdapter extends TypeAdapter { + @override + final int typeId = 3; + + @override + PlaylistModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return PlaylistModel( + id: fields[0] as String, + title: fields[1] as String, + milliseconds: fields[2] as int?, + musicList: (fields[3] as List?)?.cast(), + params: fields[4] as String?, + coverUrl: fields[5] as String?, + subtitle: fields[6] as String?, + ); + } + + @override + void write(BinaryWriter writer, PlaylistModel obj) { + writer + ..writeByte(7) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.title) + ..writeByte(2) + ..write(obj.milliseconds) + ..writeByte(3) + ..write(obj.musicList) + ..writeByte(4) + ..write(obj.params) + ..writeByte(5) + ..write(obj.coverUrl) + ..writeByte(6) + ..write(obj.subtitle); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PlaylistModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/models/browse_album_model.dart b/lib/data/models/search_result_model.dart similarity index 62% rename from lib/data/models/browse_album_model.dart rename to lib/data/models/search_result_model.dart index 24627d0..1dfa331 100644 --- a/lib/data/models/browse_album_model.dart +++ b/lib/data/models/search_result_model.dart @@ -1,57 +1,1897 @@ // Author: fengshengxiong -// Date: 2024/6/30 -// Description: Browse-专辑模型 +// Date: 2024/7/28 +// Description: 搜索结果模型 import 'dart:convert'; -class BrowseAlbumModel { +class SearchResultModel { ResponseContext? responseContext; Contents? contents; String? trackingParams; - Microformat? microformat; - StraplineThumbnailClass? background; - BrowseAlbumModel({ + SearchResultModel({ this.responseContext, this.contents, this.trackingParams, - this.microformat, - this.background, }); - factory BrowseAlbumModel.fromJson(String str) => BrowseAlbumModel.fromMap(json.decode(str)); + factory SearchResultModel.fromJson(String str) => SearchResultModel.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory BrowseAlbumModel.fromMap(Map json) => BrowseAlbumModel( + factory SearchResultModel.fromMap(Map json) => SearchResultModel( 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; +class Contents { + TabbedSearchResultsRenderer? tabbedSearchResultsRenderer; - StraplineThumbnailClass({ - this.musicThumbnailRenderer, + Contents({ + this.tabbedSearchResultsRenderer, }); - factory StraplineThumbnailClass.fromJson(String str) => StraplineThumbnailClass.fromMap(json.decode(str)); + factory Contents.fromJson(String str) => Contents.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory StraplineThumbnailClass.fromMap(Map json) => StraplineThumbnailClass( + factory Contents.fromMap(Map json) => Contents( + tabbedSearchResultsRenderer: json["tabbedSearchResultsRenderer"] == null ? null : TabbedSearchResultsRenderer.fromMap(json["tabbedSearchResultsRenderer"]), + ); + + Map toMap() => { + "tabbedSearchResultsRenderer": tabbedSearchResultsRenderer?.toMap(), + }; +} + +class TabbedSearchResultsRenderer { + List? tabs; + + TabbedSearchResultsRenderer({ + this.tabs, + }); + + factory TabbedSearchResultsRenderer.fromJson(String str) => TabbedSearchResultsRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory TabbedSearchResultsRenderer.fromMap(Map json) => TabbedSearchResultsRenderer( + tabs: json["tabs"] == null ? [] : List.from(json["tabs"]!.map((x) => Tab.fromMap(x))), + ); + + Map toMap() => { + "tabs": tabs == null ? [] : List.from(tabs!.map((x) => x.toMap())), + }; +} + +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 { + String? title; + bool? selected; + TabRendererContent? content; + String? tabIdentifier; + String? trackingParams; + + TabRenderer({ + this.title, + this.selected, + this.content, + this.tabIdentifier, + this.trackingParams, + }); + + factory TabRenderer.fromJson(String str) => TabRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory TabRenderer.fromMap(Map json) => TabRenderer( + title: json["title"], + selected: json["selected"], + content: json["content"] == null ? null : TabRendererContent.fromMap(json["content"]), + tabIdentifier: json["tabIdentifier"], + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "title": title, + "selected": selected, + "content": content?.toMap(), + "tabIdentifier": tabIdentifier, + "trackingParams": trackingParams, + }; +} + +class TabRendererContent { + SectionListRenderer? 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 : SectionListRenderer.fromMap(json["sectionListRenderer"]), + ); + + Map toMap() => { + "sectionListRenderer": sectionListRenderer?.toMap(), + }; +} + +class SectionListRenderer { + List? contents; + String? trackingParams; + SectionListRendererHeader? header; + + SectionListRenderer({ + this.contents, + this.trackingParams, + this.header, + }); + + factory SectionListRenderer.fromJson(String str) => SectionListRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SectionListRenderer.fromMap(Map json) => SectionListRenderer( + contents: json["contents"] == null ? [] : List.from(json["contents"]!.map((x) => SectionListRendererContent.fromMap(x))), + trackingParams: json["trackingParams"], + header: json["header"] == null ? null : SectionListRendererHeader.fromMap(json["header"]), + ); + + Map toMap() => { + "contents": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), + "trackingParams": trackingParams, + "header": header?.toMap(), + }; +} + +class SectionListRendererContent { + MusicCardShelfRenderer? musicCardShelfRenderer; + MusicShelfRenderer? musicShelfRenderer; + + SectionListRendererContent({ + this.musicCardShelfRenderer, + this.musicShelfRenderer, + }); + + factory SectionListRendererContent.fromJson(String str) => SectionListRendererContent.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SectionListRendererContent.fromMap(Map json) => SectionListRendererContent( + musicCardShelfRenderer: json["musicCardShelfRenderer"] == null ? null : MusicCardShelfRenderer.fromMap(json["musicCardShelfRenderer"]), + musicShelfRenderer: json["musicShelfRenderer"] == null ? null : MusicShelfRenderer.fromMap(json["musicShelfRenderer"]), + ); + + Map toMap() => { + "musicCardShelfRenderer": musicCardShelfRenderer?.toMap(), + "musicShelfRenderer": musicShelfRenderer?.toMap(), + }; +} + +class MusicCardShelfRenderer { + String? trackingParams; + MusicResponsiveListItemRendererThumbnail? thumbnail; + Title? title; + Subtitle? subtitle; + List? contents; + List? buttons; + MusicCardShelfRendererMenu? menu; + OnTap? onTap; + MusicCardShelfRendererHeader? header; + ThumbnailOverlayClass? thumbnailOverlay; + + MusicCardShelfRenderer({ + this.trackingParams, + this.thumbnail, + this.title, + this.subtitle, + this.contents, + this.buttons, + this.menu, + this.onTap, + this.header, + this.thumbnailOverlay, + }); + + factory MusicCardShelfRenderer.fromJson(String str) => MusicCardShelfRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicCardShelfRenderer.fromMap(Map json) => MusicCardShelfRenderer( + trackingParams: json["trackingParams"], + thumbnail: json["thumbnail"] == null ? null : MusicResponsiveListItemRendererThumbnail.fromMap(json["thumbnail"]), + title: json["title"] == null ? null : Title.fromMap(json["title"]), + subtitle: json["subtitle"] == null ? null : Subtitle.fromMap(json["subtitle"]), + contents: json["contents"] == null ? [] : List.from(json["contents"]!.map((x) => MusicCardShelfRendererContent.fromMap(x))), + buttons: json["buttons"] == null ? [] : List.from(json["buttons"]!.map((x) => ButtonElement.fromMap(x))), + menu: json["menu"] == null ? null : MusicCardShelfRendererMenu.fromMap(json["menu"]), + onTap: json["onTap"] == null ? null : OnTap.fromMap(json["onTap"]), + header: json["header"] == null ? null : MusicCardShelfRendererHeader.fromMap(json["header"]), + thumbnailOverlay: json["thumbnailOverlay"] == null ? null : ThumbnailOverlayClass.fromMap(json["thumbnailOverlay"]), + ); + + Map toMap() => { + "trackingParams": trackingParams, + "thumbnail": thumbnail?.toMap(), + "title": title?.toMap(), + "subtitle": subtitle?.toMap(), + "contents": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), + "buttons": buttons == null ? [] : List.from(buttons!.map((x) => x.toMap())), + "menu": menu?.toMap(), + "onTap": onTap?.toMap(), + "header": header?.toMap(), + "thumbnailOverlay": thumbnailOverlay?.toMap(), + }; +} + +class ButtonElement { + PurpleButtonRenderer? buttonRenderer; + + ButtonElement({ + this.buttonRenderer, + }); + + factory ButtonElement.fromJson(String str) => ButtonElement.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ButtonElement.fromMap(Map json) => ButtonElement( + buttonRenderer: json["buttonRenderer"] == null ? null : PurpleButtonRenderer.fromMap(json["buttonRenderer"]), + ); + + Map toMap() => { + "buttonRenderer": buttonRenderer?.toMap(), + }; +} + +class PurpleButtonRenderer { + String? style; + String? size; + bool? isDisabled; + BottomText? text; + Icon? icon; + AccessibilityAccessibility? accessibility; + String? trackingParams; + AccessibilityPauseDataClass? accessibilityData; + ButtonRendererCommand? command; + + PurpleButtonRenderer({ + this.style, + this.size, + this.isDisabled, + this.text, + this.icon, + this.accessibility, + this.trackingParams, + this.accessibilityData, + this.command, + }); + + factory PurpleButtonRenderer.fromJson(String str) => PurpleButtonRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PurpleButtonRenderer.fromMap(Map json) => PurpleButtonRenderer( + style: json["style"], + size: json["size"], + isDisabled: json["isDisabled"], + text: json["text"] == null ? null : BottomText.fromMap(json["text"]), + icon: json["icon"] == null ? null : Icon.fromMap(json["icon"]), + accessibility: json["accessibility"] == null ? null : AccessibilityAccessibility.fromMap(json["accessibility"]), + trackingParams: json["trackingParams"], + accessibilityData: json["accessibilityData"] == null ? null : AccessibilityPauseDataClass.fromMap(json["accessibilityData"]), + command: json["command"] == null ? null : ButtonRendererCommand.fromMap(json["command"]), + ); + + Map toMap() => { + "style": style, + "size": size, + "isDisabled": isDisabled, + "text": text?.toMap(), + "icon": icon?.toMap(), + "accessibility": accessibility?.toMap(), + "trackingParams": trackingParams, + "accessibilityData": accessibilityData?.toMap(), + "command": command?.toMap(), + }; +} + +class AccessibilityAccessibility { + String? label; + + AccessibilityAccessibility({ + this.label, + }); + + factory AccessibilityAccessibility.fromJson(String str) => AccessibilityAccessibility.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory AccessibilityAccessibility.fromMap(Map json) => AccessibilityAccessibility( + label: json["label"], + ); + + Map toMap() => { + "label": label, + }; +} + +class AccessibilityPauseDataClass { + AccessibilityAccessibility? accessibilityData; + + AccessibilityPauseDataClass({ + this.accessibilityData, + }); + + factory AccessibilityPauseDataClass.fromJson(String str) => AccessibilityPauseDataClass.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory AccessibilityPauseDataClass.fromMap(Map json) => AccessibilityPauseDataClass( + accessibilityData: json["accessibilityData"] == null ? null : AccessibilityAccessibility.fromMap(json["accessibilityData"]), + ); + + Map toMap() => { + "accessibilityData": accessibilityData?.toMap(), + }; +} + +class ButtonRendererCommand { + String? clickTrackingParams; + CommandWatchEndpoint? watchEndpoint; + ModalEndpoint? modalEndpoint; + + ButtonRendererCommand({ + this.clickTrackingParams, + this.watchEndpoint, + this.modalEndpoint, + }); + + factory ButtonRendererCommand.fromJson(String str) => ButtonRendererCommand.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ButtonRendererCommand.fromMap(Map json) => ButtonRendererCommand( + clickTrackingParams: json["clickTrackingParams"], + watchEndpoint: json["watchEndpoint"] == null ? null : CommandWatchEndpoint.fromMap(json["watchEndpoint"]), + modalEndpoint: json["modalEndpoint"] == null ? null : ModalEndpoint.fromMap(json["modalEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "watchEndpoint": watchEndpoint?.toMap(), + "modalEndpoint": modalEndpoint?.toMap(), + }; +} + +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 { + BottomText? title; + BottomText? 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 : BottomText.fromMap(json["title"]), + content: json["content"] == null ? null : BottomText.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 { + FluffyButtonRenderer? 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 : FluffyButtonRenderer.fromMap(json["buttonRenderer"]), + ); + + Map toMap() => { + "buttonRenderer": buttonRenderer?.toMap(), + }; +} + +class FluffyButtonRenderer { + String? style; + bool? isDisabled; + BottomText? text; + ButtonRendererNavigationEndpoint? navigationEndpoint; + String? trackingParams; + + FluffyButtonRenderer({ + this.style, + this.isDisabled, + this.text, + this.navigationEndpoint, + this.trackingParams, + }); + + factory FluffyButtonRenderer.fromJson(String str) => FluffyButtonRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory FluffyButtonRenderer.fromMap(Map json) => FluffyButtonRenderer( + style: json["style"], + isDisabled: json["isDisabled"], + text: json["text"] == null ? null : BottomText.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 BottomText { + List? runs; + + BottomText({ + this.runs, + }); + + factory BottomText.fromJson(String str) => BottomText.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory BottomText.fromMap(Map json) => BottomText( + runs: json["runs"] == null ? [] : List.from(json["runs"]!.map((x) => BottomTextRun.fromMap(x))), + ); + + Map toMap() => { + "runs": runs == null ? [] : List.from(runs!.map((x) => x.toMap())), + }; +} + +class BottomTextRun { + String? text; + + BottomTextRun({ + this.text, + }); + + factory BottomTextRun.fromJson(String str) => BottomTextRun.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory BottomTextRun.fromMap(Map json) => BottomTextRun( + text: json["text"], + ); + + Map toMap() => { + "text": text, + }; +} + +class CommandWatchEndpoint { + String? videoId; + String? params; + WatchEndpointMusicSupportedConfigs? watchEndpointMusicSupportedConfigs; + + CommandWatchEndpoint({ + this.videoId, + this.params, + this.watchEndpointMusicSupportedConfigs, + }); + + factory CommandWatchEndpoint.fromJson(String str) => CommandWatchEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory CommandWatchEndpoint.fromMap(Map json) => CommandWatchEndpoint( + videoId: json["videoId"], + params: json["params"], + watchEndpointMusicSupportedConfigs: json["watchEndpointMusicSupportedConfigs"] == null ? null : WatchEndpointMusicSupportedConfigs.fromMap(json["watchEndpointMusicSupportedConfigs"]), + ); + + Map toMap() => { + "videoId": videoId, + "params": params, + "watchEndpointMusicSupportedConfigs": watchEndpointMusicSupportedConfigs?.toMap(), + }; +} + +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 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 MusicCardShelfRendererContent { + MessageRenderer? messageRenderer; + PurpleMusicResponsiveListItemRenderer? musicResponsiveListItemRenderer; + + MusicCardShelfRendererContent({ + this.messageRenderer, + this.musicResponsiveListItemRenderer, + }); + + factory MusicCardShelfRendererContent.fromJson(String str) => MusicCardShelfRendererContent.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicCardShelfRendererContent.fromMap(Map json) => MusicCardShelfRendererContent( + messageRenderer: json["messageRenderer"] == null ? null : MessageRenderer.fromMap(json["messageRenderer"]), + musicResponsiveListItemRenderer: json["musicResponsiveListItemRenderer"] == null ? null : PurpleMusicResponsiveListItemRenderer.fromMap(json["musicResponsiveListItemRenderer"]), + ); + + Map toMap() => { + "messageRenderer": messageRenderer?.toMap(), + "musicResponsiveListItemRenderer": musicResponsiveListItemRenderer?.toMap(), + }; +} + +class MessageRenderer { + BottomText? text; + String? trackingParams; + MessageRendererStyle? style; + + MessageRenderer({ + this.text, + this.trackingParams, + this.style, + }); + + factory MessageRenderer.fromJson(String str) => MessageRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MessageRenderer.fromMap(Map json) => MessageRenderer( + text: json["text"] == null ? null : BottomText.fromMap(json["text"]), + trackingParams: json["trackingParams"], + style: json["style"] == null ? null : MessageRendererStyle.fromMap(json["style"]), + ); + + Map toMap() => { + "text": text?.toMap(), + "trackingParams": trackingParams, + "style": style?.toMap(), + }; +} + +class MessageRendererStyle { + String? value; + + MessageRendererStyle({ + this.value, + }); + + factory MessageRendererStyle.fromJson(String str) => MessageRendererStyle.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MessageRendererStyle.fromMap(Map json) => MessageRendererStyle( + value: json["value"], + ); + + Map toMap() => { + "value": value, + }; +} + +class PurpleMusicResponsiveListItemRenderer { + String? trackingParams; + MusicResponsiveListItemRendererThumbnail? thumbnail; + ThumbnailOverlayClass? overlay; + List? flexColumns; + MusicCardShelfRendererMenu? menu; + PlaylistItemData? playlistItemData; + String? flexColumnDisplayStyle; + String? itemHeight; + + PurpleMusicResponsiveListItemRenderer({ + this.trackingParams, + this.thumbnail, + this.overlay, + this.flexColumns, + this.menu, + this.playlistItemData, + this.flexColumnDisplayStyle, + this.itemHeight, + }); + + factory PurpleMusicResponsiveListItemRenderer.fromJson(String str) => PurpleMusicResponsiveListItemRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PurpleMusicResponsiveListItemRenderer.fromMap(Map json) => PurpleMusicResponsiveListItemRenderer( + trackingParams: json["trackingParams"], + thumbnail: json["thumbnail"] == null ? null : MusicResponsiveListItemRendererThumbnail.fromMap(json["thumbnail"]), + overlay: json["overlay"] == null ? null : ThumbnailOverlayClass.fromMap(json["overlay"]), + flexColumns: json["flexColumns"] == null ? [] : List.from(json["flexColumns"]!.map((x) => FlexColumn.fromMap(x))), + menu: json["menu"] == null ? null : MusicCardShelfRendererMenu.fromMap(json["menu"]), + playlistItemData: json["playlistItemData"] == null ? null : PlaylistItemData.fromMap(json["playlistItemData"]), + flexColumnDisplayStyle: json["flexColumnDisplayStyle"], + itemHeight: json["itemHeight"], + ); + + Map toMap() => { + "trackingParams": trackingParams, + "thumbnail": thumbnail?.toMap(), + "overlay": overlay?.toMap(), + "flexColumns": flexColumns == null ? [] : List.from(flexColumns!.map((x) => x.toMap())), + "menu": menu?.toMap(), + "playlistItemData": playlistItemData?.toMap(), + "flexColumnDisplayStyle": flexColumnDisplayStyle, + "itemHeight": itemHeight, + }; +} + +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; + PurpleNavigationEndpoint? 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 : PurpleNavigationEndpoint.fromMap(json["navigationEndpoint"]), + ); + + Map toMap() => { + "text": text, + "navigationEndpoint": navigationEndpoint?.toMap(), + }; +} + +class PurpleNavigationEndpoint { + String? clickTrackingParams; + OnTapWatchEndpoint? watchEndpoint; + BrowseEndpoint? browseEndpoint; + + PurpleNavigationEndpoint({ + this.clickTrackingParams, + this.watchEndpoint, + this.browseEndpoint, + }); + + factory PurpleNavigationEndpoint.fromJson(String str) => PurpleNavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PurpleNavigationEndpoint.fromMap(Map json) => PurpleNavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + watchEndpoint: json["watchEndpoint"] == null ? null : OnTapWatchEndpoint.fromMap(json["watchEndpoint"]), + browseEndpoint: json["browseEndpoint"] == null ? null : BrowseEndpoint.fromMap(json["browseEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "watchEndpoint": watchEndpoint?.toMap(), + "browseEndpoint": browseEndpoint?.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 OnTapWatchEndpoint { + String? videoId; + WatchEndpointMusicSupportedConfigs? watchEndpointMusicSupportedConfigs; + + OnTapWatchEndpoint({ + this.videoId, + this.watchEndpointMusicSupportedConfigs, + }); + + factory OnTapWatchEndpoint.fromJson(String str) => OnTapWatchEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory OnTapWatchEndpoint.fromMap(Map json) => OnTapWatchEndpoint( + videoId: json["videoId"], + watchEndpointMusicSupportedConfigs: json["watchEndpointMusicSupportedConfigs"] == null ? null : WatchEndpointMusicSupportedConfigs.fromMap(json["watchEndpointMusicSupportedConfigs"]), + ); + + Map toMap() => { + "videoId": videoId, + "watchEndpointMusicSupportedConfigs": watchEndpointMusicSupportedConfigs?.toMap(), + }; +} + +class MusicCardShelfRendererMenu { + PurpleMenuRenderer? menuRenderer; + + MusicCardShelfRendererMenu({ + this.menuRenderer, + }); + + factory MusicCardShelfRendererMenu.fromJson(String str) => MusicCardShelfRendererMenu.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicCardShelfRendererMenu.fromMap(Map json) => MusicCardShelfRendererMenu( + menuRenderer: json["menuRenderer"] == null ? null : PurpleMenuRenderer.fromMap(json["menuRenderer"]), + ); + + Map toMap() => { + "menuRenderer": menuRenderer?.toMap(), + }; +} + +class PurpleMenuRenderer { + List? items; + String? trackingParams; + AccessibilityPauseDataClass? accessibility; + + PurpleMenuRenderer({ + this.items, + this.trackingParams, + this.accessibility, + }); + + factory PurpleMenuRenderer.fromJson(String str) => PurpleMenuRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PurpleMenuRenderer.fromMap(Map json) => PurpleMenuRenderer( + items: json["items"] == null ? [] : List.from(json["items"]!.map((x) => ItemElement.fromMap(x))), + trackingParams: json["trackingParams"], + accessibility: json["accessibility"] == null ? null : AccessibilityPauseDataClass.fromMap(json["accessibility"]), + ); + + Map toMap() => { + "items": items == null ? [] : List.from(items!.map((x) => x.toMap())), + "trackingParams": trackingParams, + "accessibility": accessibility?.toMap(), + }; +} + +class ItemElement { + MenuItemRenderer? menuNavigationItemRenderer; + MenuItemRenderer? menuServiceItemRenderer; + ToggleMenuServiceItemRenderer? toggleMenuServiceItemRenderer; + + ItemElement({ + this.menuNavigationItemRenderer, + this.menuServiceItemRenderer, + this.toggleMenuServiceItemRenderer, + }); + + factory ItemElement.fromJson(String str) => ItemElement.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ItemElement.fromMap(Map json) => ItemElement( + 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 MenuItemRenderer { + BottomText? 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 : BottomText.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 MenuNavigationItemRendererNavigationEndpoint { + String? clickTrackingParams; + PurpleWatchEndpoint? watchEndpoint; + ModalEndpoint? modalEndpoint; + ShareEntityEndpoint? shareEntityEndpoint; + BrowseEndpoint? browseEndpoint; + WatchPlaylistEndpoint? watchPlaylistEndpoint; + + MenuNavigationItemRendererNavigationEndpoint({ + this.clickTrackingParams, + this.watchEndpoint, + this.modalEndpoint, + this.shareEntityEndpoint, + this.browseEndpoint, + 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 : PurpleWatchEndpoint.fromMap(json["watchEndpoint"]), + modalEndpoint: json["modalEndpoint"] == null ? null : ModalEndpoint.fromMap(json["modalEndpoint"]), + shareEntityEndpoint: json["shareEntityEndpoint"] == null ? null : ShareEntityEndpoint.fromMap(json["shareEntityEndpoint"]), + browseEndpoint: json["browseEndpoint"] == null ? null : BrowseEndpoint.fromMap(json["browseEndpoint"]), + watchPlaylistEndpoint: json["watchPlaylistEndpoint"] == null ? null : WatchPlaylistEndpoint.fromMap(json["watchPlaylistEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "watchEndpoint": watchEndpoint?.toMap(), + "modalEndpoint": modalEndpoint?.toMap(), + "shareEntityEndpoint": shareEntityEndpoint?.toMap(), + "browseEndpoint": browseEndpoint?.toMap(), + "watchPlaylistEndpoint": watchPlaylistEndpoint?.toMap(), + }; +} + +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 PurpleWatchEndpoint { + String? videoId; + String? playlistId; + String? params; + LoggingContext? loggingContext; + WatchEndpointMusicSupportedConfigs? watchEndpointMusicSupportedConfigs; + + PurpleWatchEndpoint({ + this.videoId, + this.playlistId, + this.params, + this.loggingContext, + this.watchEndpointMusicSupportedConfigs, + }); + + factory PurpleWatchEndpoint.fromJson(String str) => PurpleWatchEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PurpleWatchEndpoint.fromMap(Map json) => PurpleWatchEndpoint( + videoId: json["videoId"], + playlistId: json["playlistId"], + params: json["params"], + loggingContext: json["loggingContext"] == null ? null : LoggingContext.fromMap(json["loggingContext"]), + watchEndpointMusicSupportedConfigs: json["watchEndpointMusicSupportedConfigs"] == null ? null : WatchEndpointMusicSupportedConfigs.fromMap(json["watchEndpointMusicSupportedConfigs"]), + ); + + Map toMap() => { + "videoId": videoId, + "playlistId": playlistId, + "params": params, + "loggingContext": loggingContext?.toMap(), + "watchEndpointMusicSupportedConfigs": watchEndpointMusicSupportedConfigs?.toMap(), + }; +} + +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 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) => CommandElement.fromMap(x))), + ); + + Map toMap() => { + "queueTarget": queueTarget?.toMap(), + "queueInsertPosition": queueInsertPosition, + "commands": commands == null ? [] : List.from(commands!.map((x) => x.toMap())), + }; +} + +class CommandElement { + String? clickTrackingParams; + AddToToastAction? addToToastAction; + + CommandElement({ + this.clickTrackingParams, + this.addToToastAction, + }); + + factory CommandElement.fromJson(String str) => CommandElement.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory CommandElement.fromMap(Map json) => CommandElement( + 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 { + BottomText? 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 : BottomText.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 ToggleMenuServiceItemRenderer { + BottomText? defaultText; + Icon? defaultIcon; + DefaultServiceEndpoint? defaultServiceEndpoint; + BottomText? toggledText; + Icon? toggledIcon; + String? trackingParams; + ToggledServiceEndpoint? toggledServiceEndpoint; + + ToggleMenuServiceItemRenderer({ + this.defaultText, + this.defaultIcon, + this.defaultServiceEndpoint, + this.toggledText, + this.toggledIcon, + this.trackingParams, + this.toggledServiceEndpoint, + }); + + 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 : BottomText.fromMap(json["defaultText"]), + defaultIcon: json["defaultIcon"] == null ? null : Icon.fromMap(json["defaultIcon"]), + defaultServiceEndpoint: json["defaultServiceEndpoint"] == null ? null : DefaultServiceEndpoint.fromMap(json["defaultServiceEndpoint"]), + toggledText: json["toggledText"] == null ? null : BottomText.fromMap(json["toggledText"]), + toggledIcon: json["toggledIcon"] == null ? null : Icon.fromMap(json["toggledIcon"]), + trackingParams: json["trackingParams"], + toggledServiceEndpoint: json["toggledServiceEndpoint"] == null ? null : ToggledServiceEndpoint.fromMap(json["toggledServiceEndpoint"]), + ); + + Map toMap() => { + "defaultText": defaultText?.toMap(), + "defaultIcon": defaultIcon?.toMap(), + "defaultServiceEndpoint": defaultServiceEndpoint?.toMap(), + "toggledText": toggledText?.toMap(), + "toggledIcon": toggledIcon?.toMap(), + "trackingParams": trackingParams, + "toggledServiceEndpoint": toggledServiceEndpoint?.toMap(), + }; +} + +class DefaultServiceEndpoint { + String? clickTrackingParams; + ModalEndpoint? modalEndpoint; + + DefaultServiceEndpoint({ + this.clickTrackingParams, + this.modalEndpoint, + }); + + factory DefaultServiceEndpoint.fromJson(String str) => DefaultServiceEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory DefaultServiceEndpoint.fromMap(Map json) => DefaultServiceEndpoint( + clickTrackingParams: json["clickTrackingParams"], + modalEndpoint: json["modalEndpoint"] == null ? null : ModalEndpoint.fromMap(json["modalEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "modalEndpoint": modalEndpoint?.toMap(), + }; +} + +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; + Target? 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 : Target.fromMap(json["target"]), + ); + + Map toMap() => { + "status": status, + "target": target?.toMap(), + }; +} + +class Target { + String? playlistId; + + Target({ + this.playlistId, + }); + + factory Target.fromJson(String str) => Target.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Target.fromMap(Map json) => Target( + playlistId: json["playlistId"], + ); + + Map toMap() => { + "playlistId": playlistId, + }; +} + +class ThumbnailOverlayClass { + ThumbnailOverlayMusicItemThumbnailOverlayRenderer? musicItemThumbnailOverlayRenderer; + + ThumbnailOverlayClass({ + this.musicItemThumbnailOverlayRenderer, + }); + + factory ThumbnailOverlayClass.fromJson(String str) => ThumbnailOverlayClass.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ThumbnailOverlayClass.fromMap(Map json) => ThumbnailOverlayClass( + musicItemThumbnailOverlayRenderer: json["musicItemThumbnailOverlayRenderer"] == null ? null : ThumbnailOverlayMusicItemThumbnailOverlayRenderer.fromMap(json["musicItemThumbnailOverlayRenderer"]), + ); + + Map toMap() => { + "musicItemThumbnailOverlayRenderer": musicItemThumbnailOverlayRenderer?.toMap(), + }; +} + +class ThumbnailOverlayMusicItemThumbnailOverlayRenderer { + Background? background; + PurpleContent? content; + String? contentPosition; + String? displayStyle; + + ThumbnailOverlayMusicItemThumbnailOverlayRenderer({ + this.background, + this.content, + this.contentPosition, + this.displayStyle, + }); + + factory ThumbnailOverlayMusicItemThumbnailOverlayRenderer.fromJson(String str) => ThumbnailOverlayMusicItemThumbnailOverlayRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ThumbnailOverlayMusicItemThumbnailOverlayRenderer.fromMap(Map json) => ThumbnailOverlayMusicItemThumbnailOverlayRenderer( + background: json["background"] == null ? null : Background.fromMap(json["background"]), + content: json["content"] == null ? null : PurpleContent.fromMap(json["content"]), + contentPosition: json["contentPosition"], + displayStyle: json["displayStyle"], + ); + + Map toMap() => { + "background": background?.toMap(), + "content": content?.toMap(), + "contentPosition": contentPosition, + "displayStyle": displayStyle, + }; +} + +class Background { + VerticalGradient? verticalGradient; + + Background({ + this.verticalGradient, + }); + + factory Background.fromJson(String str) => Background.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Background.fromMap(Map json) => Background( + 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 PurpleContent { + PurpleMusicPlayButtonRenderer? musicPlayButtonRenderer; + + PurpleContent({ + this.musicPlayButtonRenderer, + }); + + factory PurpleContent.fromJson(String str) => PurpleContent.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PurpleContent.fromMap(Map json) => PurpleContent( + musicPlayButtonRenderer: json["musicPlayButtonRenderer"] == null ? null : PurpleMusicPlayButtonRenderer.fromMap(json["musicPlayButtonRenderer"]), + ); + + Map toMap() => { + "musicPlayButtonRenderer": musicPlayButtonRenderer?.toMap(), + }; +} + +class PurpleMusicPlayButtonRenderer { + OnTap? 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; + AccessibilityPauseDataClass? accessibilityPlayData; + AccessibilityPauseDataClass? accessibilityPauseData; + + PurpleMusicPlayButtonRenderer({ + 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 PurpleMusicPlayButtonRenderer.fromJson(String str) => PurpleMusicPlayButtonRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PurpleMusicPlayButtonRenderer.fromMap(Map json) => PurpleMusicPlayButtonRenderer( + playNavigationEndpoint: json["playNavigationEndpoint"] == null ? null : OnTap.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 : AccessibilityPauseDataClass.fromMap(json["accessibilityPlayData"]), + accessibilityPauseData: json["accessibilityPauseData"] == null ? null : AccessibilityPauseDataClass.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 OnTap { + String? clickTrackingParams; + OnTapWatchEndpoint? watchEndpoint; + BrowseEndpoint? browseEndpoint; + + OnTap({ + this.clickTrackingParams, + this.watchEndpoint, + this.browseEndpoint, + }); + + factory OnTap.fromJson(String str) => OnTap.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory OnTap.fromMap(Map json) => OnTap( + clickTrackingParams: json["clickTrackingParams"], + watchEndpoint: json["watchEndpoint"] == null ? null : OnTapWatchEndpoint.fromMap(json["watchEndpoint"]), + browseEndpoint: json["browseEndpoint"] == null ? null : BrowseEndpoint.fromMap(json["browseEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "watchEndpoint": watchEndpoint?.toMap(), + "browseEndpoint": browseEndpoint?.toMap(), + }; +} + +class PlaylistItemData { + String? videoId; + + PlaylistItemData({ + this.videoId, + }); + + factory PlaylistItemData.fromJson(String str) => PlaylistItemData.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PlaylistItemData.fromMap(Map json) => PlaylistItemData( + videoId: json["videoId"], + ); + + Map toMap() => { + "videoId": videoId, + }; +} + +class MusicResponsiveListItemRendererThumbnail { + MusicThumbnailRenderer? musicThumbnailRenderer; + + MusicResponsiveListItemRendererThumbnail({ + this.musicThumbnailRenderer, + }); + + factory MusicResponsiveListItemRendererThumbnail.fromJson(String str) => MusicResponsiveListItemRendererThumbnail.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicResponsiveListItemRendererThumbnail.fromMap(Map json) => MusicResponsiveListItemRendererThumbnail( musicThumbnailRenderer: json["musicThumbnailRenderer"] == null ? null : MusicThumbnailRenderer.fromMap(json["musicThumbnailRenderer"]), ); @@ -140,125 +1980,179 @@ class ThumbnailElement { }; } -class Contents { - TwoColumnBrowseResultsRenderer? twoColumnBrowseResultsRenderer; +class MusicCardShelfRendererHeader { + MusicCardShelfHeaderBasicRenderer? musicCardShelfHeaderBasicRenderer; - Contents({ - this.twoColumnBrowseResultsRenderer, + MusicCardShelfRendererHeader({ + this.musicCardShelfHeaderBasicRenderer, }); - factory Contents.fromJson(String str) => Contents.fromMap(json.decode(str)); + factory MusicCardShelfRendererHeader.fromJson(String str) => MusicCardShelfRendererHeader.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory Contents.fromMap(Map json) => Contents( - twoColumnBrowseResultsRenderer: json["twoColumnBrowseResultsRenderer"] == null ? null : TwoColumnBrowseResultsRenderer.fromMap(json["twoColumnBrowseResultsRenderer"]), + factory MusicCardShelfRendererHeader.fromMap(Map json) => MusicCardShelfRendererHeader( + musicCardShelfHeaderBasicRenderer: json["musicCardShelfHeaderBasicRenderer"] == null ? null : MusicCardShelfHeaderBasicRenderer.fromMap(json["musicCardShelfHeaderBasicRenderer"]), ); Map toMap() => { - "twoColumnBrowseResultsRenderer": twoColumnBrowseResultsRenderer?.toMap(), + "musicCardShelfHeaderBasicRenderer": musicCardShelfHeaderBasicRenderer?.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; +class MusicCardShelfHeaderBasicRenderer { + BottomText? title; String? trackingParams; - SecondaryContentsSectionListRenderer({ - this.contents, + MusicCardShelfHeaderBasicRenderer({ + this.title, this.trackingParams, }); - factory SecondaryContentsSectionListRenderer.fromJson(String str) => SecondaryContentsSectionListRenderer.fromMap(json.decode(str)); + factory MusicCardShelfHeaderBasicRenderer.fromJson(String str) => MusicCardShelfHeaderBasicRenderer.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))), + factory MusicCardShelfHeaderBasicRenderer.fromMap(Map json) => MusicCardShelfHeaderBasicRenderer( + title: json["title"] == null ? null : BottomText.fromMap(json["title"]), trackingParams: json["trackingParams"], ); Map toMap() => { - "contents": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), + "title": title?.toMap(), "trackingParams": trackingParams, }; } -class PurpleContent { - MusicShelfRenderer? musicShelfRenderer; +class Subtitle { + List? runs; - PurpleContent({ - this.musicShelfRenderer, + Subtitle({ + this.runs, }); - factory PurpleContent.fromJson(String str) => PurpleContent.fromMap(json.decode(str)); + factory Subtitle.fromJson(String str) => Subtitle.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory PurpleContent.fromMap(Map json) => PurpleContent( - musicShelfRenderer: json["musicShelfRenderer"] == null ? null : MusicShelfRenderer.fromMap(json["musicShelfRenderer"]), + factory Subtitle.fromMap(Map json) => Subtitle( + runs: json["runs"] == null ? [] : List.from(json["runs"]!.map((x) => SubtitleRun.fromMap(x))), ); Map toMap() => { - "musicShelfRenderer": musicShelfRenderer?.toMap(), + "runs": runs == null ? [] : List.from(runs!.map((x) => x.toMap())), + }; +} + +class SubtitleRun { + String? text; + MusicResponsiveListItemRendererNavigationEndpoint? navigationEndpoint; + + SubtitleRun({ + this.text, + this.navigationEndpoint, + }); + + factory SubtitleRun.fromJson(String str) => SubtitleRun.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SubtitleRun.fromMap(Map json) => SubtitleRun( + text: json["text"], + navigationEndpoint: json["navigationEndpoint"] == null ? null : MusicResponsiveListItemRendererNavigationEndpoint.fromMap(json["navigationEndpoint"]), + ); + + Map toMap() => { + "text": text, + "navigationEndpoint": navigationEndpoint?.toMap(), + }; +} + +class MusicResponsiveListItemRendererNavigationEndpoint { + String? clickTrackingParams; + BrowseEndpoint? browseEndpoint; + + MusicResponsiveListItemRendererNavigationEndpoint({ + this.clickTrackingParams, + this.browseEndpoint, + }); + + factory MusicResponsiveListItemRendererNavigationEndpoint.fromJson(String str) => MusicResponsiveListItemRendererNavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicResponsiveListItemRendererNavigationEndpoint.fromMap(Map json) => MusicResponsiveListItemRendererNavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + browseEndpoint: json["browseEndpoint"] == null ? null : BrowseEndpoint.fromMap(json["browseEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "browseEndpoint": browseEndpoint?.toMap(), + }; +} + +class Title { + List? runs; + + Title({ + this.runs, + }); + + factory Title.fromJson(String str) => Title.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Title.fromMap(Map json) => Title( + runs: json["runs"] == null ? [] : List.from(json["runs"]!.map((x) => FluffyRun.fromMap(x))), + ); + + Map toMap() => { + "runs": runs == null ? [] : List.from(runs!.map((x) => x.toMap())), + }; +} + +class FluffyRun { + String? text; + OnTap? navigationEndpoint; + + FluffyRun({ + this.text, + this.navigationEndpoint, + }); + + factory FluffyRun.fromJson(String str) => FluffyRun.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory FluffyRun.fromMap(Map json) => FluffyRun( + text: json["text"], + navigationEndpoint: json["navigationEndpoint"] == null ? null : OnTap.fromMap(json["navigationEndpoint"]), + ); + + Map toMap() => { + "text": text, + "navigationEndpoint": navigationEndpoint?.toMap(), }; } class MusicShelfRenderer { + BottomText? title; List? contents; String? trackingParams; + BottomText? bottomText; + Endpoint? bottomEndpoint; + List? continuations; ShelfDivider? shelfDivider; - bool? contentsMultiSelectable; MusicShelfRenderer({ + this.title, this.contents, this.trackingParams, + this.bottomText, + this.bottomEndpoint, + this.continuations, this.shelfDivider, - this.contentsMultiSelectable, }); factory MusicShelfRenderer.fromJson(String str) => MusicShelfRenderer.fromMap(json.decode(str)); @@ -266,22 +2160,76 @@ class MusicShelfRenderer { String toJson() => json.encode(toMap()); factory MusicShelfRenderer.fromMap(Map json) => MusicShelfRenderer( + title: json["title"] == null ? null : BottomText.fromMap(json["title"]), contents: json["contents"] == null ? [] : List.from(json["contents"]!.map((x) => MusicShelfRendererContent.fromMap(x))), trackingParams: json["trackingParams"], + bottomText: json["bottomText"] == null ? null : BottomText.fromMap(json["bottomText"]), + bottomEndpoint: json["bottomEndpoint"] == null ? null : Endpoint.fromMap(json["bottomEndpoint"]), + continuations: json["continuations"] == null ? [] : List.from(json["continuations"]!.map((x) => Continuation.fromMap(x))), shelfDivider: json["shelfDivider"] == null ? null : ShelfDivider.fromMap(json["shelfDivider"]), - contentsMultiSelectable: json["contentsMultiSelectable"], ); Map toMap() => { + "title": title?.toMap(), "contents": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), "trackingParams": trackingParams, + "bottomText": bottomText?.toMap(), + "bottomEndpoint": bottomEndpoint?.toMap(), + "continuations": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), "shelfDivider": shelfDivider?.toMap(), - "contentsMultiSelectable": contentsMultiSelectable, + }; +} + +class Endpoint { + String? clickTrackingParams; + SearchEndpoint? searchEndpoint; + + Endpoint({ + this.clickTrackingParams, + this.searchEndpoint, + }); + + factory Endpoint.fromJson(String str) => Endpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Endpoint.fromMap(Map json) => Endpoint( + clickTrackingParams: json["clickTrackingParams"], + searchEndpoint: json["searchEndpoint"] == null ? null : SearchEndpoint.fromMap(json["searchEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "searchEndpoint": searchEndpoint?.toMap(), + }; +} + +class SearchEndpoint { + String? query; + String? params; + + SearchEndpoint({ + this.query, + this.params, + }); + + factory SearchEndpoint.fromJson(String str) => SearchEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SearchEndpoint.fromMap(Map json) => SearchEndpoint( + query: json["query"], + params: json["params"], + ); + + Map toMap() => { + "query": query, + "params": params, }; } class MusicShelfRendererContent { - MusicResponsiveListItemRenderer? musicResponsiveListItemRenderer; + FluffyMusicResponsiveListItemRenderer? musicResponsiveListItemRenderer; MusicShelfRendererContent({ this.musicResponsiveListItemRenderer, @@ -292,7 +2240,7 @@ class MusicShelfRendererContent { String toJson() => json.encode(toMap()); factory MusicShelfRendererContent.fromMap(Map json) => MusicShelfRendererContent( - musicResponsiveListItemRenderer: json["musicResponsiveListItemRenderer"] == null ? null : MusicResponsiveListItemRenderer.fromMap(json["musicResponsiveListItemRenderer"]), + musicResponsiveListItemRenderer: json["musicResponsiveListItemRenderer"] == null ? null : FluffyMusicResponsiveListItemRenderer.fromMap(json["musicResponsiveListItemRenderer"]), ); Map toMap() => { @@ -300,391 +2248,71 @@ class MusicShelfRendererContent { }; } -class MusicResponsiveListItemRenderer { +class FluffyMusicResponsiveListItemRenderer { String? trackingParams; - Overlay? overlay; + MusicResponsiveListItemRendererThumbnail? thumbnail; + PurpleOverlay? overlay; List? flexColumns; - List? fixedColumns; - Menu? menu; + PurpleMenu? menu; PlaylistItemData? playlistItemData; + String? flexColumnDisplayStyle; String? itemHeight; - Index? index; - MultiSelectCheckbox? multiSelectCheckbox; + MusicResponsiveListItemRendererNavigationEndpoint? navigationEndpoint; - MusicResponsiveListItemRenderer({ + FluffyMusicResponsiveListItemRenderer({ this.trackingParams, + this.thumbnail, this.overlay, this.flexColumns, - this.fixedColumns, this.menu, this.playlistItemData, + this.flexColumnDisplayStyle, this.itemHeight, - this.index, - this.multiSelectCheckbox, + this.navigationEndpoint, }); - factory MusicResponsiveListItemRenderer.fromJson(String str) => MusicResponsiveListItemRenderer.fromMap(json.decode(str)); + factory FluffyMusicResponsiveListItemRenderer.fromJson(String str) => FluffyMusicResponsiveListItemRenderer.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory MusicResponsiveListItemRenderer.fromMap(Map json) => MusicResponsiveListItemRenderer( + factory FluffyMusicResponsiveListItemRenderer.fromMap(Map json) => FluffyMusicResponsiveListItemRenderer( trackingParams: json["trackingParams"], - overlay: json["overlay"] == null ? null : Overlay.fromMap(json["overlay"]), + thumbnail: json["thumbnail"] == null ? null : MusicResponsiveListItemRendererThumbnail.fromMap(json["thumbnail"]), + overlay: json["overlay"] == null ? null : PurpleOverlay.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"]), + menu: json["menu"] == null ? null : PurpleMenu.fromMap(json["menu"]), playlistItemData: json["playlistItemData"] == null ? null : PlaylistItemData.fromMap(json["playlistItemData"]), + flexColumnDisplayStyle: json["flexColumnDisplayStyle"], itemHeight: json["itemHeight"], - index: json["index"] == null ? null : Index.fromMap(json["index"]), - multiSelectCheckbox: json["multiSelectCheckbox"] == null ? null : MultiSelectCheckbox.fromMap(json["multiSelectCheckbox"]), + navigationEndpoint: json["navigationEndpoint"] == null ? null : MusicResponsiveListItemRendererNavigationEndpoint.fromMap(json["navigationEndpoint"]), ); Map toMap() => { "trackingParams": trackingParams, + "thumbnail": thumbnail?.toMap(), "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(), + "flexColumnDisplayStyle": flexColumnDisplayStyle, "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; +class PurpleMenu { + FluffyMenuRenderer? menuRenderer; - 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({ + PurpleMenu({ this.menuRenderer, }); - factory Menu.fromJson(String str) => Menu.fromMap(json.decode(str)); + factory PurpleMenu.fromJson(String str) => PurpleMenu.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory Menu.fromMap(Map json) => Menu( - menuRenderer: json["menuRenderer"] == null ? null : MenuMenuRenderer.fromMap(json["menuRenderer"]), + factory PurpleMenu.fromMap(Map json) => PurpleMenu( + menuRenderer: json["menuRenderer"] == null ? null : FluffyMenuRenderer.fromMap(json["menuRenderer"]), ); Map toMap() => { @@ -692,691 +2320,35 @@ class Menu { }; } -class MenuMenuRenderer { - List? items; +class FluffyMenuRenderer { + List? items; String? trackingParams; + AccessibilityPauseDataClass? accessibility; List? topLevelButtons; - Accessibility? accessibility; - MenuMenuRenderer({ + FluffyMenuRenderer({ this.items, this.trackingParams, - this.topLevelButtons, this.accessibility, + this.topLevelButtons, }); - factory MenuMenuRenderer.fromJson(String str) => MenuMenuRenderer.fromMap(json.decode(str)); + factory FluffyMenuRenderer.fromJson(String str) => FluffyMenuRenderer.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))), + factory FluffyMenuRenderer.fromMap(Map json) => FluffyMenuRenderer( + items: json["items"] == null ? [] : List.from(json["items"]!.map((x) => ItemElement.fromMap(x))), trackingParams: json["trackingParams"], + accessibility: json["accessibility"] == null ? null : AccessibilityPauseDataClass.fromMap(json["accessibility"]), 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, + "topLevelButtons": topLevelButtons == null ? [] : List.from(topLevelButtons!.map((x) => x.toMap())), }; } @@ -1401,12 +2373,12 @@ class TopLevelButton { } class LikeButtonRenderer { - LikeButtonRendererTarget? target; + PlaylistItemData? target; String? likeStatus; String? trackingParams; bool? likesAllowed; - DefaultNavigationEndpoint? dislikeNavigationEndpoint; - DefaultNavigationEndpoint? likeCommand; + DefaultServiceEndpoint? dislikeNavigationEndpoint; + DefaultServiceEndpoint? likeCommand; LikeButtonRenderer({ this.target, @@ -1422,12 +2394,12 @@ class LikeButtonRenderer { String toJson() => json.encode(toMap()); factory LikeButtonRenderer.fromMap(Map json) => LikeButtonRenderer( - target: json["target"] == null ? null : LikeButtonRendererTarget.fromMap(json["target"]), + target: json["target"] == null ? null : PlaylistItemData.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"]), + dislikeNavigationEndpoint: json["dislikeNavigationEndpoint"] == null ? null : DefaultServiceEndpoint.fromMap(json["dislikeNavigationEndpoint"]), + likeCommand: json["likeCommand"] == null ? null : DefaultServiceEndpoint.fromMap(json["likeCommand"]), ); Map toMap() => { @@ -1440,159 +2412,19 @@ class LikeButtonRenderer { }; } -class DefaultNavigationEndpoint { - String? clickTrackingParams; - ModalEndpoint? modalEndpoint; +class PurpleOverlay { + PurpleMusicItemThumbnailOverlayRenderer? musicItemThumbnailOverlayRenderer; - 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({ + PurpleOverlay({ this.musicItemThumbnailOverlayRenderer, }); - factory Overlay.fromJson(String str) => Overlay.fromMap(json.decode(str)); + factory PurpleOverlay.fromJson(String str) => PurpleOverlay.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory Overlay.fromMap(Map json) => Overlay( - musicItemThumbnailOverlayRenderer: json["musicItemThumbnailOverlayRenderer"] == null ? null : MusicItemThumbnailOverlayRenderer.fromMap(json["musicItemThumbnailOverlayRenderer"]), + factory PurpleOverlay.fromMap(Map json) => PurpleOverlay( + musicItemThumbnailOverlayRenderer: json["musicItemThumbnailOverlayRenderer"] == null ? null : PurpleMusicItemThumbnailOverlayRenderer.fromMap(json["musicItemThumbnailOverlayRenderer"]), ); Map toMap() => { @@ -1600,26 +2432,26 @@ class Overlay { }; } -class MusicItemThumbnailOverlayRenderer { - MusicItemThumbnailOverlayRendererBackground? background; - MusicItemThumbnailOverlayRendererContent? content; +class PurpleMusicItemThumbnailOverlayRenderer { + Background? background; + FluffyContent? content; String? contentPosition; String? displayStyle; - MusicItemThumbnailOverlayRenderer({ + PurpleMusicItemThumbnailOverlayRenderer({ this.background, this.content, this.contentPosition, this.displayStyle, }); - factory MusicItemThumbnailOverlayRenderer.fromJson(String str) => MusicItemThumbnailOverlayRenderer.fromMap(json.decode(str)); + factory PurpleMusicItemThumbnailOverlayRenderer.fromJson(String str) => PurpleMusicItemThumbnailOverlayRenderer.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"]), + factory PurpleMusicItemThumbnailOverlayRenderer.fromMap(Map json) => PurpleMusicItemThumbnailOverlayRenderer( + background: json["background"] == null ? null : Background.fromMap(json["background"]), + content: json["content"] == null ? null : FluffyContent.fromMap(json["content"]), contentPosition: json["contentPosition"], displayStyle: json["displayStyle"], ); @@ -1632,59 +2464,19 @@ class MusicItemThumbnailOverlayRenderer { }; } -class MusicItemThumbnailOverlayRendererBackground { - VerticalGradient? verticalGradient; +class FluffyContent { + FluffyMusicPlayButtonRenderer? musicPlayButtonRenderer; - 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({ + FluffyContent({ this.musicPlayButtonRenderer, }); - factory MusicItemThumbnailOverlayRendererContent.fromJson(String str) => MusicItemThumbnailOverlayRendererContent.fromMap(json.decode(str)); + factory FluffyContent.fromJson(String str) => FluffyContent.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory MusicItemThumbnailOverlayRendererContent.fromMap(Map json) => MusicItemThumbnailOverlayRendererContent( - musicPlayButtonRenderer: json["musicPlayButtonRenderer"] == null ? null : ContentMusicPlayButtonRenderer.fromMap(json["musicPlayButtonRenderer"]), + factory FluffyContent.fromMap(Map json) => FluffyContent( + musicPlayButtonRenderer: json["musicPlayButtonRenderer"] == null ? null : FluffyMusicPlayButtonRenderer.fromMap(json["musicPlayButtonRenderer"]), ); Map toMap() => { @@ -1692,8 +2484,8 @@ class MusicItemThumbnailOverlayRendererContent { }; } -class ContentMusicPlayButtonRenderer { - NavigationEndpoint? playNavigationEndpoint; +class FluffyMusicPlayButtonRenderer { + PlayNavigationEndpoint? playNavigationEndpoint; String? trackingParams; Icon? playIcon; Icon? pauseIcon; @@ -1706,10 +2498,10 @@ class ContentMusicPlayButtonRenderer { int? activeScaleFactor; String? buttonSize; String? rippleTarget; - Accessibility? accessibilityPlayData; - Accessibility? accessibilityPauseData; + AccessibilityPauseDataClass? accessibilityPlayData; + AccessibilityPauseDataClass? accessibilityPauseData; - ContentMusicPlayButtonRenderer({ + FluffyMusicPlayButtonRenderer({ this.playNavigationEndpoint, this.trackingParams, this.playIcon, @@ -1727,12 +2519,12 @@ class ContentMusicPlayButtonRenderer { this.accessibilityPauseData, }); - factory ContentMusicPlayButtonRenderer.fromJson(String str) => ContentMusicPlayButtonRenderer.fromMap(json.decode(str)); + factory FluffyMusicPlayButtonRenderer.fromJson(String str) => FluffyMusicPlayButtonRenderer.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory ContentMusicPlayButtonRenderer.fromMap(Map json) => ContentMusicPlayButtonRenderer( - playNavigationEndpoint: json["playNavigationEndpoint"] == null ? null : NavigationEndpoint.fromMap(json["playNavigationEndpoint"]), + factory FluffyMusicPlayButtonRenderer.fromMap(Map json) => FluffyMusicPlayButtonRenderer( + playNavigationEndpoint: json["playNavigationEndpoint"] == null ? null : PlayNavigationEndpoint.fromMap(json["playNavigationEndpoint"]), trackingParams: json["trackingParams"], playIcon: json["playIcon"] == null ? null : Icon.fromMap(json["playIcon"]), pauseIcon: json["pauseIcon"] == null ? null : Icon.fromMap(json["pauseIcon"]), @@ -1745,8 +2537,8 @@ class ContentMusicPlayButtonRenderer { 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"]), + accessibilityPlayData: json["accessibilityPlayData"] == null ? null : AccessibilityPauseDataClass.fromMap(json["accessibilityPlayData"]), + accessibilityPauseData: json["accessibilityPauseData"] == null ? null : AccessibilityPauseDataClass.fromMap(json["accessibilityPauseData"]), ); Map toMap() => { @@ -1768,27 +2560,91 @@ class ContentMusicPlayButtonRenderer { }; } -class PlaylistItemData { - String? playlistSetVideoId; - String? videoId; +class PlayNavigationEndpoint { + String? clickTrackingParams; + CommandWatchEndpoint? watchEndpoint; + WatchPlaylistEndpoint? watchPlaylistEndpoint; - PlaylistItemData({ - this.playlistSetVideoId, - this.videoId, + PlayNavigationEndpoint({ + this.clickTrackingParams, + this.watchEndpoint, + this.watchPlaylistEndpoint, }); - factory PlaylistItemData.fromJson(String str) => PlaylistItemData.fromMap(json.decode(str)); + factory PlayNavigationEndpoint.fromJson(String str) => PlayNavigationEndpoint.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory PlaylistItemData.fromMap(Map json) => PlaylistItemData( - playlistSetVideoId: json["playlistSetVideoId"], - videoId: json["videoId"], + factory PlayNavigationEndpoint.fromMap(Map json) => PlayNavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + watchEndpoint: json["watchEndpoint"] == null ? null : CommandWatchEndpoint.fromMap(json["watchEndpoint"]), + watchPlaylistEndpoint: json["watchPlaylistEndpoint"] == null ? null : WatchPlaylistEndpoint.fromMap(json["watchPlaylistEndpoint"]), ); Map toMap() => { - "playlistSetVideoId": playlistSetVideoId, - "videoId": videoId, + "clickTrackingParams": clickTrackingParams, + "watchEndpoint": watchEndpoint?.toMap(), + "watchPlaylistEndpoint": watchPlaylistEndpoint?.toMap(), + }; +} + +class Continuation { + NextContinuationData? nextContinuationData; + + Continuation({ + this.nextContinuationData, + }); + + Continuation copyWith({ + NextContinuationData? nextContinuationData, + }) => + Continuation( + nextContinuationData: nextContinuationData ?? this.nextContinuationData, + ); + + factory Continuation.fromJson(String str) => Continuation.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Continuation.fromMap(Map json) => Continuation( + nextContinuationData: json["nextContinuationData"] == null ? null : NextContinuationData.fromMap(json["nextContinuationData"]), + ); + + Map toMap() => { + "nextContinuationData": nextContinuationData?.toMap(), + }; +} + +class NextContinuationData { + String? continuation; + String? clickTrackingParams; + + NextContinuationData({ + this.continuation, + this.clickTrackingParams, + }); + + NextContinuationData copyWith({ + String? continuation, + String? clickTrackingParams, + }) => + NextContinuationData( + continuation: continuation ?? this.continuation, + clickTrackingParams: clickTrackingParams ?? this.clickTrackingParams, + ); + + factory NextContinuationData.fromJson(String str) => NextContinuationData.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory NextContinuationData.fromMap(Map json) => NextContinuationData( + continuation: json["continuation"], + clickTrackingParams: json["clickTrackingParams"], + ); + + Map toMap() => { + "continuation": continuation, + "clickTrackingParams": clickTrackingParams, }; } @@ -1832,801 +2688,151 @@ class MusicShelfDividerRenderer { }; } -class Tab { - TabRenderer? tabRenderer; +class SectionListRendererHeader { + ChipCloudRenderer? chipCloudRenderer; - Tab({ - this.tabRenderer, + SectionListRendererHeader({ + this.chipCloudRenderer, }); - factory Tab.fromJson(String str) => Tab.fromMap(json.decode(str)); + factory SectionListRendererHeader.fromJson(String str) => SectionListRendererHeader.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory Tab.fromMap(Map json) => Tab( - tabRenderer: json["tabRenderer"] == null ? null : TabRenderer.fromMap(json["tabRenderer"]), + factory SectionListRendererHeader.fromMap(Map json) => SectionListRendererHeader( + chipCloudRenderer: json["chipCloudRenderer"] == null ? null : ChipCloudRenderer.fromMap(json["chipCloudRenderer"]), ); Map toMap() => { - "tabRenderer": tabRenderer?.toMap(), + "chipCloudRenderer": chipCloudRenderer?.toMap(), }; } -class TabRenderer { - TabRendererContent? content; +class ChipCloudRenderer { + List? chips; + int? collapsedRowCount; String? trackingParams; + bool? horizontalScrollable; - TabRenderer({ - this.content, + ChipCloudRenderer({ + this.chips, + this.collapsedRowCount, this.trackingParams, + this.horizontalScrollable, }); - factory TabRenderer.fromJson(String str) => TabRenderer.fromMap(json.decode(str)); + factory ChipCloudRenderer.fromJson(String str) => ChipCloudRenderer.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory TabRenderer.fromMap(Map json) => TabRenderer( - content: json["content"] == null ? null : TabRendererContent.fromMap(json["content"]), + factory ChipCloudRenderer.fromMap(Map json) => ChipCloudRenderer( + chips: json["chips"] == null ? [] : List.from(json["chips"]!.map((x) => Chip.fromMap(x))), + collapsedRowCount: json["collapsedRowCount"], trackingParams: json["trackingParams"], + horizontalScrollable: json["horizontalScrollable"], ); Map toMap() => { - "content": content?.toMap(), + "chips": chips == null ? [] : List.from(chips!.map((x) => x.toMap())), + "collapsedRowCount": collapsedRowCount, "trackingParams": trackingParams, + "horizontalScrollable": horizontalScrollable, }; } -class TabRendererContent { - ContentSectionListRenderer? sectionListRenderer; +class Chip { + ChipCloudChipRenderer? chipCloudChipRenderer; - TabRendererContent({ - this.sectionListRenderer, + Chip({ + this.chipCloudChipRenderer, }); - factory TabRendererContent.fromJson(String str) => TabRendererContent.fromMap(json.decode(str)); + factory Chip.fromJson(String str) => Chip.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory TabRendererContent.fromMap(Map json) => TabRendererContent( - sectionListRenderer: json["sectionListRenderer"] == null ? null : ContentSectionListRenderer.fromMap(json["sectionListRenderer"]), + factory Chip.fromMap(Map json) => Chip( + chipCloudChipRenderer: json["chipCloudChipRenderer"] == null ? null : ChipCloudChipRenderer.fromMap(json["chipCloudChipRenderer"]), ); Map toMap() => { - "sectionListRenderer": sectionListRenderer?.toMap(), + "chipCloudChipRenderer": chipCloudChipRenderer?.toMap(), }; } -class ContentSectionListRenderer { - List? contents; +class ChipCloudChipRenderer { + ChipCloudChipRendererStyle? style; + BottomText? text; + Endpoint? navigationEndpoint; String? trackingParams; + AccessibilityPauseDataClass? accessibilityData; + bool? isSelected; + String? uniqueId; - ContentSectionListRenderer({ - this.contents, + ChipCloudChipRenderer({ + this.style, + this.text, + this.navigationEndpoint, 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, + this.isSelected, + this.uniqueId, }); - factory ButtonToggleButtonRenderer.fromJson(String str) => ButtonToggleButtonRenderer.fromMap(json.decode(str)); + factory ChipCloudChipRenderer.fromJson(String str) => ChipCloudChipRenderer.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"]), + factory ChipCloudChipRenderer.fromMap(Map json) => ChipCloudChipRenderer( + style: json["style"] == null ? null : ChipCloudChipRendererStyle.fromMap(json["style"]), + text: json["text"] == null ? null : BottomText.fromMap(json["text"]), + navigationEndpoint: json["navigationEndpoint"] == null ? null : Endpoint.fromMap(json["navigationEndpoint"]), 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"]), + accessibilityData: json["accessibilityData"] == null ? null : AccessibilityPauseDataClass.fromMap(json["accessibilityData"]), + isSelected: json["isSelected"], + uniqueId: json["uniqueId"], ); Map toMap() => { - "isToggled": isToggled, - "isDisabled": isDisabled, - "defaultIcon": defaultIcon?.toMap(), - "toggledIcon": toggledIcon?.toMap(), + "style": style?.toMap(), + "text": text?.toMap(), + "navigationEndpoint": navigationEndpoint?.toMap(), "trackingParams": trackingParams, - "defaultNavigationEndpoint": defaultNavigationEndpoint?.toMap(), "accessibilityData": accessibilityData?.toMap(), - "toggledAccessibilityData": toggledAccessibilityData?.toMap(), + "isSelected": isSelected, + "uniqueId": uniqueId, }; } -class MusicResponsiveHeaderRendererDescription { - MusicDescriptionShelfRenderer? musicDescriptionShelfRenderer; +class ChipCloudChipRendererStyle { + String? styleType; - MusicResponsiveHeaderRendererDescription({ - this.musicDescriptionShelfRenderer, + ChipCloudChipRendererStyle({ + this.styleType, }); - factory MusicResponsiveHeaderRendererDescription.fromJson(String str) => MusicResponsiveHeaderRendererDescription.fromMap(json.decode(str)); + factory ChipCloudChipRendererStyle.fromJson(String str) => ChipCloudChipRendererStyle.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); - factory MusicResponsiveHeaderRendererDescription.fromMap(Map json) => MusicResponsiveHeaderRendererDescription( - musicDescriptionShelfRenderer: json["musicDescriptionShelfRenderer"] == null ? null : MusicDescriptionShelfRenderer.fromMap(json["musicDescriptionShelfRenderer"]), + factory ChipCloudChipRendererStyle.fromMap(Map json) => ChipCloudChipRendererStyle( + styleType: json["styleType"], ); 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, + "styleType": styleType, }; } class ResponseContext { String? visitorData; List? serviceTrackingParams; + int? maxAgeSeconds; ResponseContext({ this.visitorData, this.serviceTrackingParams, + this.maxAgeSeconds, }); factory ResponseContext.fromJson(String str) => ResponseContext.fromMap(json.decode(str)); @@ -2636,11 +2842,13 @@ class ResponseContext { factory ResponseContext.fromMap(Map json) => ResponseContext( visitorData: json["visitorData"], serviceTrackingParams: json["serviceTrackingParams"] == null ? [] : List.from(json["serviceTrackingParams"]!.map((x) => ServiceTrackingParam.fromMap(x))), + maxAgeSeconds: json["maxAgeSeconds"], ); Map toMap() => { "visitorData": visitorData, "serviceTrackingParams": serviceTrackingParams == null ? [] : List.from(serviceTrackingParams!.map((x) => x.toMap())), + "maxAgeSeconds": maxAgeSeconds, }; } diff --git a/lib/data/models/search_result_more_model.dart b/lib/data/models/search_result_more_model.dart new file mode 100644 index 0000000..a6387e8 --- /dev/null +++ b/lib/data/models/search_result_more_model.dart @@ -0,0 +1,1925 @@ +// Author: fengshengxiong +// Date: 2024/7/29 +// Description: 搜索结果更多模型 + +import 'dart:convert'; + +class SearchResultMoreModel { + ResponseContext? responseContext; + ContinuationContents? continuationContents; + String? trackingParams; + SearchResultMoreModelHeader? header; + + SearchResultMoreModel({ + this.responseContext, + this.continuationContents, + this.trackingParams, + this.header, + }); + + factory SearchResultMoreModel.fromJson(String str) => SearchResultMoreModel.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SearchResultMoreModel.fromMap(Map json) => SearchResultMoreModel( + responseContext: json["responseContext"] == null ? null : ResponseContext.fromMap(json["responseContext"]), + continuationContents: json["continuationContents"] == null ? null : ContinuationContents.fromMap(json["continuationContents"]), + trackingParams: json["trackingParams"], + header: json["header"] == null ? null : SearchResultMoreModelHeader.fromMap(json["header"]), + ); + + Map toMap() => { + "responseContext": responseContext?.toMap(), + "continuationContents": continuationContents?.toMap(), + "trackingParams": trackingParams, + "header": header?.toMap(), + }; +} + +class ContinuationContents { + MusicShelfContinuation? musicShelfContinuation; + + ContinuationContents({ + this.musicShelfContinuation, + }); + + factory ContinuationContents.fromJson(String str) => ContinuationContents.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ContinuationContents.fromMap(Map json) => ContinuationContents( + musicShelfContinuation: json["musicShelfContinuation"] == null ? null : MusicShelfContinuation.fromMap(json["musicShelfContinuation"]), + ); + + Map toMap() => { + "musicShelfContinuation": musicShelfContinuation?.toMap(), + }; +} + +class MusicShelfContinuation { + List? contents; + String? trackingParams; + List? continuations; + ShelfDivider? shelfDivider; + bool? autoReloadWhenEmpty; + + MusicShelfContinuation({ + this.contents, + this.trackingParams, + this.continuations, + this.shelfDivider, + this.autoReloadWhenEmpty, + }); + + factory MusicShelfContinuation.fromJson(String str) => MusicShelfContinuation.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicShelfContinuation.fromMap(Map json) => MusicShelfContinuation( + contents: json["contents"] == null ? [] : List.from(json["contents"]!.map((x) => ContentElement.fromMap(x))), + trackingParams: json["trackingParams"], + continuations: json["continuations"] == null ? [] : List.from(json["continuations"]!.map((x) => Continuation.fromMap(x))), + shelfDivider: json["shelfDivider"] == null ? null : ShelfDivider.fromMap(json["shelfDivider"]), + autoReloadWhenEmpty: json["autoReloadWhenEmpty"], + ); + + Map toMap() => { + "contents": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), + "trackingParams": trackingParams, + "continuations": continuations == null ? [] : List.from(continuations!.map((x) => x.toMap())), + "shelfDivider": shelfDivider?.toMap(), + "autoReloadWhenEmpty": autoReloadWhenEmpty, + }; +} + +class ContentElement { + MusicResponsiveListItemRenderer? musicResponsiveListItemRenderer; + + ContentElement({ + this.musicResponsiveListItemRenderer, + }); + + factory ContentElement.fromJson(String str) => ContentElement.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ContentElement.fromMap(Map json) => ContentElement( + musicResponsiveListItemRenderer: json["musicResponsiveListItemRenderer"] == null ? null : MusicResponsiveListItemRenderer.fromMap(json["musicResponsiveListItemRenderer"]), + ); + + Map toMap() => { + "musicResponsiveListItemRenderer": musicResponsiveListItemRenderer?.toMap(), + }; +} + +class MusicResponsiveListItemRenderer { + String? trackingParams; + MusicResponsiveListItemRendererThumbnail? thumbnail; + Overlay? overlay; + List? flexColumns; + Menu? menu; + PlaylistItemData? playlistItemData; + String? flexColumnDisplayStyle; + NavigationEndpoint? navigationEndpoint; + String? itemHeight; + + MusicResponsiveListItemRenderer({ + this.trackingParams, + this.thumbnail, + this.overlay, + this.flexColumns, + this.menu, + this.playlistItemData, + this.flexColumnDisplayStyle, + this.navigationEndpoint, + this.itemHeight, + }); + + factory MusicResponsiveListItemRenderer.fromJson(String str) => MusicResponsiveListItemRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicResponsiveListItemRenderer.fromMap(Map json) => MusicResponsiveListItemRenderer( + trackingParams: json["trackingParams"], + thumbnail: json["thumbnail"] == null ? null : MusicResponsiveListItemRendererThumbnail.fromMap(json["thumbnail"]), + overlay: json["overlay"] == null ? null : Overlay.fromMap(json["overlay"]), + flexColumns: json["flexColumns"] == null ? [] : List.from(json["flexColumns"]!.map((x) => FlexColumn.fromMap(x))), + menu: json["menu"] == null ? null : Menu.fromMap(json["menu"]), + playlistItemData: json["playlistItemData"] == null ? null : PlaylistItemData.fromMap(json["playlistItemData"]), + flexColumnDisplayStyle: json["flexColumnDisplayStyle"], + navigationEndpoint: json["navigationEndpoint"] == null ? null : NavigationEndpoint.fromMap(json["navigationEndpoint"]), + itemHeight: json["itemHeight"], + ); + + Map toMap() => { + "trackingParams": trackingParams, + "thumbnail": thumbnail?.toMap(), + "overlay": overlay?.toMap(), + "flexColumns": flexColumns == null ? [] : List.from(flexColumns!.map((x) => x.toMap())), + "menu": menu?.toMap(), + "playlistItemData": playlistItemData?.toMap(), + "flexColumnDisplayStyle": flexColumnDisplayStyle, + "navigationEndpoint": navigationEndpoint?.toMap(), + "itemHeight": itemHeight, + }; +} + +class NavigationEndpoint { + String? clickTrackingParams; + BrowseEndpoint? browseEndpoint; + + NavigationEndpoint({ + this.clickTrackingParams, + this.browseEndpoint, + }); + + factory NavigationEndpoint.fromJson(String str) => NavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory NavigationEndpoint.fromMap(Map json) => NavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + browseEndpoint: json["browseEndpoint"] == null ? null : BrowseEndpoint.fromMap(json["browseEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "browseEndpoint": browseEndpoint?.toMap(), + }; +} + +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 { + MusicResponsiveListItemFlexColumnRendererText? 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 : MusicResponsiveListItemFlexColumnRendererText.fromMap(json["text"]), + displayPriority: json["displayPriority"], + ); + + Map toMap() => { + "text": text?.toMap(), + "displayPriority": displayPriority, + }; +} + +class MusicResponsiveListItemFlexColumnRendererText { + List? runs; + + MusicResponsiveListItemFlexColumnRendererText({ + this.runs, + }); + + factory MusicResponsiveListItemFlexColumnRendererText.fromJson(String str) => MusicResponsiveListItemFlexColumnRendererText.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicResponsiveListItemFlexColumnRendererText.fromMap(Map json) => MusicResponsiveListItemFlexColumnRendererText( + 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; + RunNavigationEndpoint? 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 : RunNavigationEndpoint.fromMap(json["navigationEndpoint"]), + ); + + Map toMap() => { + "text": text, + "navigationEndpoint": navigationEndpoint?.toMap(), + }; +} + +class RunNavigationEndpoint { + String? clickTrackingParams; + PlayNavigationEndpointWatchEndpoint? watchEndpoint; + BrowseEndpoint? browseEndpoint; + + RunNavigationEndpoint({ + this.clickTrackingParams, + this.watchEndpoint, + this.browseEndpoint, + }); + + factory RunNavigationEndpoint.fromJson(String str) => RunNavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory RunNavigationEndpoint.fromMap(Map json) => RunNavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + watchEndpoint: json["watchEndpoint"] == null ? null : PlayNavigationEndpointWatchEndpoint.fromMap(json["watchEndpoint"]), + browseEndpoint: json["browseEndpoint"] == null ? null : BrowseEndpoint.fromMap(json["browseEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "watchEndpoint": watchEndpoint?.toMap(), + "browseEndpoint": browseEndpoint?.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 PlayNavigationEndpointWatchEndpoint { + String? videoId; + WatchEndpointMusicSupportedConfigs? watchEndpointMusicSupportedConfigs; + + PlayNavigationEndpointWatchEndpoint({ + this.videoId, + this.watchEndpointMusicSupportedConfigs, + }); + + factory PlayNavigationEndpointWatchEndpoint.fromJson(String str) => PlayNavigationEndpointWatchEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PlayNavigationEndpointWatchEndpoint.fromMap(Map json) => PlayNavigationEndpointWatchEndpoint( + videoId: json["videoId"], + watchEndpointMusicSupportedConfigs: json["watchEndpointMusicSupportedConfigs"] == null ? null : WatchEndpointMusicSupportedConfigs.fromMap(json["watchEndpointMusicSupportedConfigs"]), + ); + + Map toMap() => { + "videoId": videoId, + "watchEndpointMusicSupportedConfigs": watchEndpointMusicSupportedConfigs?.toMap(), + }; +} + +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 { + MenuRenderer? 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 : MenuRenderer.fromMap(json["menuRenderer"]), + ); + + Map toMap() => { + "menuRenderer": menuRenderer?.toMap(), + }; +} + +class MenuRenderer { + List? items; + String? trackingParams; + Accessibility? accessibility; + + MenuRenderer({ + this.items, + this.trackingParams, + this.accessibility, + }); + + factory MenuRenderer.fromJson(String str) => MenuRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MenuRenderer.fromMap(Map json) => MenuRenderer( + items: json["items"] == null ? [] : List.from(json["items"]!.map((x) => ItemElement.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 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 ItemElement { + MenuItemRenderer? menuNavigationItemRenderer; + MenuItemRenderer? menuServiceItemRenderer; + ToggleMenuServiceItemRenderer? toggleMenuServiceItemRenderer; + + ItemElement({ + this.menuNavigationItemRenderer, + this.menuServiceItemRenderer, + this.toggleMenuServiceItemRenderer, + }); + + factory ItemElement.fromJson(String str) => ItemElement.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ItemElement.fromMap(Map json) => ItemElement( + 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 MenuItemRenderer { + DefaultTextClass? 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 : DefaultTextClass.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; + PurpleWatchEndpoint? watchEndpoint; + ModalEndpoint? modalEndpoint; + BrowseEndpoint? browseEndpoint; + ShareEntityEndpoint? shareEntityEndpoint; + + MenuNavigationItemRendererNavigationEndpoint({ + this.clickTrackingParams, + this.watchEndpoint, + this.modalEndpoint, + this.browseEndpoint, + this.shareEntityEndpoint, + }); + + 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 : PurpleWatchEndpoint.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"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "watchEndpoint": watchEndpoint?.toMap(), + "modalEndpoint": modalEndpoint?.toMap(), + "browseEndpoint": browseEndpoint?.toMap(), + "shareEntityEndpoint": shareEntityEndpoint?.toMap(), + }; +} + +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 { + DefaultTextClass? title; + DefaultTextClass? content; + Button? 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 : DefaultTextClass.fromMap(json["title"]), + content: json["content"] == null ? null : DefaultTextClass.fromMap(json["content"]), + button: json["button"] == null ? null : Button.fromMap(json["button"]), + ); + + Map toMap() => { + "title": title?.toMap(), + "content": content?.toMap(), + "button": button?.toMap(), + }; +} + +class Button { + ButtonRenderer? buttonRenderer; + + Button({ + this.buttonRenderer, + }); + + factory Button.fromJson(String str) => Button.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Button.fromMap(Map json) => Button( + buttonRenderer: json["buttonRenderer"] == null ? null : ButtonRenderer.fromMap(json["buttonRenderer"]), + ); + + Map toMap() => { + "buttonRenderer": buttonRenderer?.toMap(), + }; +} + +class ButtonRenderer { + String? style; + bool? isDisabled; + DefaultTextClass? 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 : DefaultTextClass.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 DefaultTextClass { + List? runs; + + DefaultTextClass({ + this.runs, + }); + + factory DefaultTextClass.fromJson(String str) => DefaultTextClass.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory DefaultTextClass.fromMap(Map json) => DefaultTextClass( + runs: json["runs"] == null ? [] : List.from(json["runs"]!.map((x) => DefaultTextRun.fromMap(x))), + ); + + Map toMap() => { + "runs": runs == null ? [] : List.from(runs!.map((x) => x.toMap())), + }; +} + +class DefaultTextRun { + String? text; + + DefaultTextRun({ + this.text, + }); + + factory DefaultTextRun.fromJson(String str) => DefaultTextRun.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory DefaultTextRun.fromMap(Map json) => DefaultTextRun( + text: json["text"], + ); + + Map toMap() => { + "text": text, + }; +} + +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 PurpleWatchEndpoint { + String? videoId; + String? playlistId; + String? params; + LoggingContext? loggingContext; + WatchEndpointMusicSupportedConfigs? watchEndpointMusicSupportedConfigs; + + PurpleWatchEndpoint({ + this.videoId, + this.playlistId, + this.params, + this.loggingContext, + this.watchEndpointMusicSupportedConfigs, + }); + + factory PurpleWatchEndpoint.fromJson(String str) => PurpleWatchEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PurpleWatchEndpoint.fromMap(Map json) => PurpleWatchEndpoint( + videoId: json["videoId"], + playlistId: json["playlistId"], + params: json["params"], + loggingContext: json["loggingContext"] == null ? null : LoggingContext.fromMap(json["loggingContext"]), + watchEndpointMusicSupportedConfigs: json["watchEndpointMusicSupportedConfigs"] == null ? null : WatchEndpointMusicSupportedConfigs.fromMap(json["watchEndpointMusicSupportedConfigs"]), + ); + + Map toMap() => { + "videoId": videoId, + "playlistId": playlistId, + "params": params, + "loggingContext": loggingContext?.toMap(), + "watchEndpointMusicSupportedConfigs": watchEndpointMusicSupportedConfigs?.toMap(), + }; +} + +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 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 { + DefaultTextClass? 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 : DefaultTextClass.fromMap(json["successResponseText"]), + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "successResponseText": successResponseText?.toMap(), + "trackingParams": trackingParams, + }; +} + +class QueueTarget { + String? videoId; + OnEmptyQueue? onEmptyQueue; + + QueueTarget({ + this.videoId, + this.onEmptyQueue, + }); + + 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"]), + ); + + Map toMap() => { + "videoId": videoId, + "onEmptyQueue": onEmptyQueue?.toMap(), + }; +} + +class OnEmptyQueue { + String? clickTrackingParams; + PlaylistItemData? 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 : PlaylistItemData.fromMap(json["watchEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "watchEndpoint": watchEndpoint?.toMap(), + }; +} + +class PlaylistItemData { + String? videoId; + + PlaylistItemData({ + this.videoId, + }); + + factory PlaylistItemData.fromJson(String str) => PlaylistItemData.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PlaylistItemData.fromMap(Map json) => PlaylistItemData( + videoId: json["videoId"], + ); + + Map toMap() => { + "videoId": videoId, + }; +} + +class ToggleMenuServiceItemRenderer { + DefaultTextClass? defaultText; + Icon? defaultIcon; + DefaultServiceEndpoint? defaultServiceEndpoint; + DefaultTextClass? toggledText; + Icon? toggledIcon; + String? trackingParams; + + ToggleMenuServiceItemRenderer({ + this.defaultText, + this.defaultIcon, + this.defaultServiceEndpoint, + this.toggledText, + this.toggledIcon, + 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 : DefaultTextClass.fromMap(json["defaultText"]), + defaultIcon: json["defaultIcon"] == null ? null : Icon.fromMap(json["defaultIcon"]), + defaultServiceEndpoint: json["defaultServiceEndpoint"] == null ? null : DefaultServiceEndpoint.fromMap(json["defaultServiceEndpoint"]), + toggledText: json["toggledText"] == null ? null : DefaultTextClass.fromMap(json["toggledText"]), + toggledIcon: json["toggledIcon"] == null ? null : Icon.fromMap(json["toggledIcon"]), + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "defaultText": defaultText?.toMap(), + "defaultIcon": defaultIcon?.toMap(), + "defaultServiceEndpoint": defaultServiceEndpoint?.toMap(), + "toggledText": toggledText?.toMap(), + "toggledIcon": toggledIcon?.toMap(), + "trackingParams": trackingParams, + }; +} + +class DefaultServiceEndpoint { + String? clickTrackingParams; + ModalEndpoint? modalEndpoint; + + DefaultServiceEndpoint({ + this.clickTrackingParams, + this.modalEndpoint, + }); + + factory DefaultServiceEndpoint.fromJson(String str) => DefaultServiceEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory DefaultServiceEndpoint.fromMap(Map json) => DefaultServiceEndpoint( + clickTrackingParams: json["clickTrackingParams"], + modalEndpoint: json["modalEndpoint"] == null ? null : ModalEndpoint.fromMap(json["modalEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "modalEndpoint": modalEndpoint?.toMap(), + }; +} + +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 { + Background? 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 : Background.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 Background { + VerticalGradient? verticalGradient; + + Background({ + this.verticalGradient, + }); + + factory Background.fromJson(String str) => Background.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Background.fromMap(Map json) => Background( + 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 { + MusicPlayButtonRenderer? 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 : MusicPlayButtonRenderer.fromMap(json["musicPlayButtonRenderer"]), + ); + + Map toMap() => { + "musicPlayButtonRenderer": musicPlayButtonRenderer?.toMap(), + }; +} + +class MusicPlayButtonRenderer { + PlayNavigationEndpoint? 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; + + MusicPlayButtonRenderer({ + 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 MusicPlayButtonRenderer.fromJson(String str) => MusicPlayButtonRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicPlayButtonRenderer.fromMap(Map json) => MusicPlayButtonRenderer( + playNavigationEndpoint: json["playNavigationEndpoint"] == null ? null : PlayNavigationEndpoint.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 PlayNavigationEndpoint { + String? clickTrackingParams; + PlayNavigationEndpointWatchEndpoint? watchEndpoint; + + PlayNavigationEndpoint({ + this.clickTrackingParams, + this.watchEndpoint, + }); + + factory PlayNavigationEndpoint.fromJson(String str) => PlayNavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PlayNavigationEndpoint.fromMap(Map json) => PlayNavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + watchEndpoint: json["watchEndpoint"] == null ? null : PlayNavigationEndpointWatchEndpoint.fromMap(json["watchEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "watchEndpoint": watchEndpoint?.toMap(), + }; +} + +class MusicResponsiveListItemRendererThumbnail { + MusicThumbnailRenderer? musicThumbnailRenderer; + + MusicResponsiveListItemRendererThumbnail({ + this.musicThumbnailRenderer, + }); + + factory MusicResponsiveListItemRendererThumbnail.fromJson(String str) => MusicResponsiveListItemRendererThumbnail.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicResponsiveListItemRendererThumbnail.fromMap(Map json) => MusicResponsiveListItemRendererThumbnail( + 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 Continuation { + NextContinuationData? nextContinuationData; + + Continuation({ + this.nextContinuationData, + }); + + factory Continuation.fromJson(String str) => Continuation.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Continuation.fromMap(Map json) => Continuation( + nextContinuationData: json["nextContinuationData"] == null ? null : NextContinuationData.fromMap(json["nextContinuationData"]), + ); + + Map toMap() => { + "nextContinuationData": nextContinuationData?.toMap(), + }; +} + +class NextContinuationData { + String? continuation; + String? clickTrackingParams; + + NextContinuationData({ + this.continuation, + this.clickTrackingParams, + }); + + factory NextContinuationData.fromJson(String str) => NextContinuationData.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory NextContinuationData.fromMap(Map json) => NextContinuationData( + continuation: json["continuation"], + clickTrackingParams: json["clickTrackingParams"], + ); + + Map toMap() => { + "continuation": continuation, + "clickTrackingParams": clickTrackingParams, + }; +} + +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 SearchResultMoreModelHeader { + MusicHeaderRenderer? musicHeaderRenderer; + + SearchResultMoreModelHeader({ + this.musicHeaderRenderer, + }); + + factory SearchResultMoreModelHeader.fromJson(String str) => SearchResultMoreModelHeader.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SearchResultMoreModelHeader.fromMap(Map json) => SearchResultMoreModelHeader( + musicHeaderRenderer: json["musicHeaderRenderer"] == null ? null : MusicHeaderRenderer.fromMap(json["musicHeaderRenderer"]), + ); + + Map toMap() => { + "musicHeaderRenderer": musicHeaderRenderer?.toMap(), + }; +} + +class MusicHeaderRenderer { + MusicHeaderRendererHeader? header; + String? trackingParams; + + MusicHeaderRenderer({ + this.header, + this.trackingParams, + }); + + factory MusicHeaderRenderer.fromJson(String str) => MusicHeaderRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicHeaderRenderer.fromMap(Map json) => MusicHeaderRenderer( + header: json["header"] == null ? null : MusicHeaderRendererHeader.fromMap(json["header"]), + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "header": header?.toMap(), + "trackingParams": trackingParams, + }; +} + +class MusicHeaderRendererHeader { + ChipCloudRenderer? chipCloudRenderer; + + MusicHeaderRendererHeader({ + this.chipCloudRenderer, + }); + + factory MusicHeaderRendererHeader.fromJson(String str) => MusicHeaderRendererHeader.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory MusicHeaderRendererHeader.fromMap(Map json) => MusicHeaderRendererHeader( + chipCloudRenderer: json["chipCloudRenderer"] == null ? null : ChipCloudRenderer.fromMap(json["chipCloudRenderer"]), + ); + + Map toMap() => { + "chipCloudRenderer": chipCloudRenderer?.toMap(), + }; +} + +class ChipCloudRenderer { + List? chips; + int? collapsedRowCount; + String? trackingParams; + bool? horizontalScrollable; + + ChipCloudRenderer({ + this.chips, + this.collapsedRowCount, + this.trackingParams, + this.horizontalScrollable, + }); + + factory ChipCloudRenderer.fromJson(String str) => ChipCloudRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ChipCloudRenderer.fromMap(Map json) => ChipCloudRenderer( + chips: json["chips"] == null ? [] : List.from(json["chips"]!.map((x) => Chip.fromMap(x))), + collapsedRowCount: json["collapsedRowCount"], + trackingParams: json["trackingParams"], + horizontalScrollable: json["horizontalScrollable"], + ); + + Map toMap() => { + "chips": chips == null ? [] : List.from(chips!.map((x) => x.toMap())), + "collapsedRowCount": collapsedRowCount, + "trackingParams": trackingParams, + "horizontalScrollable": horizontalScrollable, + }; +} + +class Chip { + ChipCloudChipRenderer? chipCloudChipRenderer; + + Chip({ + this.chipCloudChipRenderer, + }); + + factory Chip.fromJson(String str) => Chip.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Chip.fromMap(Map json) => Chip( + chipCloudChipRenderer: json["chipCloudChipRenderer"] == null ? null : ChipCloudChipRenderer.fromMap(json["chipCloudChipRenderer"]), + ); + + Map toMap() => { + "chipCloudChipRenderer": chipCloudChipRenderer?.toMap(), + }; +} + +class ChipCloudChipRenderer { + Style? style; + ChipCloudChipRendererNavigationEndpoint? navigationEndpoint; + String? trackingParams; + Icon? icon; + Accessibility? accessibilityData; + bool? isSelected; + DefaultTextClass? text; + String? uniqueId; + + ChipCloudChipRenderer({ + this.style, + this.navigationEndpoint, + this.trackingParams, + this.icon, + this.accessibilityData, + this.isSelected, + this.text, + this.uniqueId, + }); + + factory ChipCloudChipRenderer.fromJson(String str) => ChipCloudChipRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ChipCloudChipRenderer.fromMap(Map json) => ChipCloudChipRenderer( + style: json["style"] == null ? null : Style.fromMap(json["style"]), + navigationEndpoint: json["navigationEndpoint"] == null ? null : ChipCloudChipRendererNavigationEndpoint.fromMap(json["navigationEndpoint"]), + trackingParams: json["trackingParams"], + icon: json["icon"] == null ? null : Icon.fromMap(json["icon"]), + accessibilityData: json["accessibilityData"] == null ? null : Accessibility.fromMap(json["accessibilityData"]), + isSelected: json["isSelected"], + text: json["text"] == null ? null : DefaultTextClass.fromMap(json["text"]), + uniqueId: json["uniqueId"], + ); + + Map toMap() => { + "style": style?.toMap(), + "navigationEndpoint": navigationEndpoint?.toMap(), + "trackingParams": trackingParams, + "icon": icon?.toMap(), + "accessibilityData": accessibilityData?.toMap(), + "isSelected": isSelected, + "text": text?.toMap(), + "uniqueId": uniqueId, + }; +} + +class ChipCloudChipRendererNavigationEndpoint { + String? clickTrackingParams; + SearchEndpoint? searchEndpoint; + + ChipCloudChipRendererNavigationEndpoint({ + this.clickTrackingParams, + this.searchEndpoint, + }); + + factory ChipCloudChipRendererNavigationEndpoint.fromJson(String str) => ChipCloudChipRendererNavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory ChipCloudChipRendererNavigationEndpoint.fromMap(Map json) => ChipCloudChipRendererNavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + searchEndpoint: json["searchEndpoint"] == null ? null : SearchEndpoint.fromMap(json["searchEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "searchEndpoint": searchEndpoint?.toMap(), + }; +} + +class SearchEndpoint { + String? query; + String? params; + + SearchEndpoint({ + this.query, + this.params, + }); + + factory SearchEndpoint.fromJson(String str) => SearchEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SearchEndpoint.fromMap(Map json) => SearchEndpoint( + query: json["query"], + params: json["params"], + ); + + Map toMap() => { + "query": query, + "params": params, + }; +} + +class Style { + String? styleType; + + Style({ + this.styleType, + }); + + factory Style.fromJson(String str) => Style.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Style.fromMap(Map json) => Style( + styleType: json["styleType"], + ); + + Map toMap() => { + "styleType": styleType, + }; +} + +class ResponseContext { + String? visitorData; + List? serviceTrackingParams; + int? maxAgeSeconds; + + ResponseContext({ + this.visitorData, + this.serviceTrackingParams, + this.maxAgeSeconds, + }); + + 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))), + maxAgeSeconds: json["maxAgeSeconds"], + ); + + Map toMap() => { + "visitorData": visitorData, + "serviceTrackingParams": serviceTrackingParams == null ? [] : List.from(serviceTrackingParams!.map((x) => x.toMap())), + "maxAgeSeconds": maxAgeSeconds, + }; +} + +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/models/search_result_tabbar_model.dart b/lib/data/models/search_result_tabbar_model.dart new file mode 100644 index 0000000..21d8c6f --- /dev/null +++ b/lib/data/models/search_result_tabbar_model.dart @@ -0,0 +1,52 @@ +// Author: fengshengxiong +// Date: 2024/7/28 +// Description: 搜索结果TabBar模型 + +import 'dart:convert'; + +import 'package:tone_snap/data/models/music_model.dart'; + +class SearchResultTabBarModel { + String? title; + String? params; + String? uniqueId; + List? musicList; + + SearchResultTabBarModel({ + this.title, + this.params, + this.uniqueId, + this.musicList, + }); + + SearchResultTabBarModel copyWith({ + String? title, + String? params, + String? uniqueId, + List? musicList, + }) => + SearchResultTabBarModel( + title: title ?? this.title, + params: params ?? this.params, + uniqueId: uniqueId ?? this.uniqueId, + musicList: musicList ?? this.musicList, + ); + + factory SearchResultTabBarModel.fromJson(String str) => SearchResultTabBarModel.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SearchResultTabBarModel.fromMap(Map json) => SearchResultTabBarModel( + title: json["title"], + params: json["params"], + uniqueId: json["uniqueId"], + musicList: json["musicList"] == null ? [] : List.from(json["musicList"]!.map((x) => MusicModel.fromMap(x))), + ); + + Map toMap() => { + "title": title, + "params": params, + "uniqueId": uniqueId, + "musicList": musicList == null ? [] : List.from(musicList!.map((x) => x.toMap())), + }; +} diff --git a/lib/data/models/search_suggestions_model.dart b/lib/data/models/search_suggestions_model.dart new file mode 100644 index 0000000..44e5a97 --- /dev/null +++ b/lib/data/models/search_suggestions_model.dart @@ -0,0 +1,305 @@ +// Author: fengshengxiong +// Date: 2024/7/28 +// Description: 搜索建议模型 + +import 'dart:convert'; + +class SearchSuggestionsModel { + ResponseContext? responseContext; + List? contents; + String? trackingParams; + + SearchSuggestionsModel({ + this.responseContext, + this.contents, + this.trackingParams, + }); + + factory SearchSuggestionsModel.fromJson(String str) => SearchSuggestionsModel.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SearchSuggestionsModel.fromMap(Map json) => SearchSuggestionsModel( + responseContext: json["responseContext"] == null ? null : ResponseContext.fromMap(json["responseContext"]), + contents: json["contents"] == null ? [] : List.from(json["contents"]!.map((x) => SearchSuggestionsModelContent.fromMap(x))), + trackingParams: json["trackingParams"], + ); + + Map toMap() => { + "responseContext": responseContext?.toMap(), + "contents": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), + "trackingParams": trackingParams, + }; +} + +class SearchSuggestionsModelContent { + SearchSuggestionsSectionRenderer? searchSuggestionsSectionRenderer; + + SearchSuggestionsModelContent({ + this.searchSuggestionsSectionRenderer, + }); + + factory SearchSuggestionsModelContent.fromJson(String str) => SearchSuggestionsModelContent.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SearchSuggestionsModelContent.fromMap(Map json) => SearchSuggestionsModelContent( + searchSuggestionsSectionRenderer: json["searchSuggestionsSectionRenderer"] == null ? null : SearchSuggestionsSectionRenderer.fromMap(json["searchSuggestionsSectionRenderer"]), + ); + + Map toMap() => { + "searchSuggestionsSectionRenderer": searchSuggestionsSectionRenderer?.toMap(), + }; +} + +class SearchSuggestionsSectionRenderer { + List? contents; + + SearchSuggestionsSectionRenderer({ + this.contents, + }); + + factory SearchSuggestionsSectionRenderer.fromJson(String str) => SearchSuggestionsSectionRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SearchSuggestionsSectionRenderer.fromMap(Map json) => SearchSuggestionsSectionRenderer( + contents: json["contents"] == null ? [] : List.from(json["contents"]!.map((x) => SearchSuggestionsSectionRendererContent.fromMap(x))), + ); + + Map toMap() => { + "contents": contents == null ? [] : List.from(contents!.map((x) => x.toMap())), + }; +} + +class SearchSuggestionsSectionRendererContent { + SearchSuggestionRenderer? searchSuggestionRenderer; + + SearchSuggestionsSectionRendererContent({ + this.searchSuggestionRenderer, + }); + + factory SearchSuggestionsSectionRendererContent.fromJson(String str) => SearchSuggestionsSectionRendererContent.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SearchSuggestionsSectionRendererContent.fromMap(Map json) => SearchSuggestionsSectionRendererContent( + searchSuggestionRenderer: json["searchSuggestionRenderer"] == null ? null : SearchSuggestionRenderer.fromMap(json["searchSuggestionRenderer"]), + ); + + Map toMap() => { + "searchSuggestionRenderer": searchSuggestionRenderer?.toMap(), + }; +} + +class SearchSuggestionRenderer { + Suggestion? suggestion; + NavigationEndpoint? navigationEndpoint; + String? trackingParams; + Icon? icon; + + SearchSuggestionRenderer({ + this.suggestion, + this.navigationEndpoint, + this.trackingParams, + this.icon, + }); + + factory SearchSuggestionRenderer.fromJson(String str) => SearchSuggestionRenderer.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SearchSuggestionRenderer.fromMap(Map json) => SearchSuggestionRenderer( + suggestion: json["suggestion"] == null ? null : Suggestion.fromMap(json["suggestion"]), + navigationEndpoint: json["navigationEndpoint"] == null ? null : NavigationEndpoint.fromMap(json["navigationEndpoint"]), + trackingParams: json["trackingParams"], + icon: json["icon"] == null ? null : Icon.fromMap(json["icon"]), + ); + + Map toMap() => { + "suggestion": suggestion?.toMap(), + "navigationEndpoint": navigationEndpoint?.toMap(), + "trackingParams": trackingParams, + "icon": icon?.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 NavigationEndpoint { + String? clickTrackingParams; + SearchEndpoint? searchEndpoint; + + NavigationEndpoint({ + this.clickTrackingParams, + this.searchEndpoint, + }); + + factory NavigationEndpoint.fromJson(String str) => NavigationEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory NavigationEndpoint.fromMap(Map json) => NavigationEndpoint( + clickTrackingParams: json["clickTrackingParams"], + searchEndpoint: json["searchEndpoint"] == null ? null : SearchEndpoint.fromMap(json["searchEndpoint"]), + ); + + Map toMap() => { + "clickTrackingParams": clickTrackingParams, + "searchEndpoint": searchEndpoint?.toMap(), + }; +} + +class SearchEndpoint { + String? query; + + SearchEndpoint({ + this.query, + }); + + factory SearchEndpoint.fromJson(String str) => SearchEndpoint.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory SearchEndpoint.fromMap(Map json) => SearchEndpoint( + query: json["query"], + ); + + Map toMap() => { + "query": query, + }; +} + +class Suggestion { + List? runs; + + Suggestion({ + this.runs, + }); + + factory Suggestion.fromJson(String str) => Suggestion.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Suggestion.fromMap(Map json) => Suggestion( + runs: json["runs"] == null ? [] : List.from(json["runs"]!.map((x) => Run.fromMap(x))), + ); + + Map toMap() => { + "runs": runs == null ? [] : List.from(runs!.map((x) => x.toMap())), + }; +} + +class Run { + String? text; + bool? bold; + + Run({ + this.text, + this.bold, + }); + + factory Run.fromJson(String str) => Run.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory Run.fromMap(Map json) => Run( + text: json["text"], + bold: json["bold"], + ); + + Map toMap() => { + "text": text, + "bold": bold, + }; +} + +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/network/dio_client.dart b/lib/data/network/dio_client.dart index f49ecf9..2a5341d 100644 --- a/lib/data/network/dio_client.dart +++ b/lib/data/network/dio_client.dart @@ -44,8 +44,8 @@ class DioClient { _dio = Dio(); final baseOptions = BaseOptions( baseUrl: MusicApi.baseUrl, - connectTimeout: const Duration(seconds: 15), - receiveTimeout: const Duration(seconds: 15), + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 30), ); _dio.options = baseOptions; _dio.interceptors.add(DioInterceptor()); @@ -103,6 +103,7 @@ class DioClient { Future download( String urlPath, dynamic savePath, { + bool showToast = false, ProgressCallback? onReceiveProgress, Map? queryParameters, CancelToken? cancelToken, @@ -125,7 +126,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(baseError.message); } } @@ -134,19 +135,19 @@ class DioClient { if (e.runtimeType == DioException) { switch ((e as DioException).type) { case DioExceptionType.connectionTimeout: - return OtherError(statusCode: -1, statusMessage: 'connection timed out'); + return OtherError(statusCode: DioExceptionType.connectionTimeout.index, statusMessage: 'Connection timed out'); case DioExceptionType.sendTimeout: - return OtherError(statusCode: -1, statusMessage: 'send timeout'); + return OtherError(statusCode: DioExceptionType.sendTimeout.index, statusMessage: 'Send timeout'); case DioExceptionType.receiveTimeout: - return OtherError(statusCode: -1, statusMessage: 'receive timeout'); + return OtherError(statusCode: DioExceptionType.receiveTimeout.index, statusMessage: 'Receive timeout'); case DioExceptionType.badCertificate: - return OtherError(statusCode: -1, statusMessage: 'certificate error'); + return OtherError(statusCode: DioExceptionType.badCertificate.index, statusMessage: 'Certificate error'); case DioExceptionType.cancel: - return OtherError(statusCode: -1, statusMessage: 'request canceled'); + return OtherError(statusCode: DioExceptionType.cancel.index, statusMessage: 'Request canceled'); case DioExceptionType.connectionError: - return OtherError(statusCode: -1, statusMessage: 'connection error'); + return OtherError(statusCode: DioExceptionType.connectionError.index, statusMessage: 'Connection error'); case DioExceptionType.unknown: - return OtherError(statusCode: -1, statusMessage: 'unknown error'); + return OtherError(statusCode: DioExceptionType.unknown.index, statusMessage: 'Unknown error'); case DioExceptionType.badResponse: final response = e.response; if (response!.statusCode == 401) { @@ -161,7 +162,7 @@ class DioClient { } } } - return OtherError(statusCode: -1, statusMessage: 'unknown error'); + return OtherError(statusCode: -1, statusMessage: 'Unknown error'); } } diff --git a/lib/data/storage/collect_playlists_box.dart b/lib/data/storage/collect_playlists_box.dart new file mode 100644 index 0000000..80b78a5 --- /dev/null +++ b/lib/data/storage/collect_playlists_box.dart @@ -0,0 +1,59 @@ +// Author: fengshengxiong +// Date: 2024/5/8 +// Description: 收藏播放列表盒子 + +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:tone_snap/data/models/playlist_model.dart'; +import 'package:tone_snap/data/storage/hive_storage.dart'; + +class CollectPlaylistsBox { + CollectPlaylistsBox._(); + + static final CollectPlaylistsBox _instance = CollectPlaylistsBox._(); + + factory CollectPlaylistsBox() { + return _instance; + } + + /// 声明盒子 + /// 注意, main函数中这个盒子已经打开, 可以进行存储操作 + final _box = Hive.box(collectPlaylistsBox); + + /// 获取列表 + List getList() { + return _box.values.toList(); + } + + /// 获取倒序列表 + List getReversedList() { + return _box.values.toList().reversed.toList(); + } + + /// 添加数据 + Future add({required String id, required String title, String? params, String? coverUrl, String? subtitle}) async { + return await _box.add(PlaylistModel( + id: id, + title: title, + params: params, + coverUrl: coverUrl, + subtitle: subtitle, + )); + } + + /// 删除 + Future delete(String id) async { + var list = getList(); + var model = list.firstWhereOrNull((e) => e.id == id); + if (model != null) { + await _box.deleteAt(list.indexOf(model)); + await _box.flush(); + } + } + + /// 清空所有数据 + Future clear() async { + await _box.clear(); + await _box.flush(); + } +} diff --git a/lib/data/storage/favorite_box.dart b/lib/data/storage/favorite_box.dart index 6ba3837..09b1a28 100644 --- a/lib/data/storage/favorite_box.dart +++ b/lib/data/storage/favorite_box.dart @@ -1,7 +1,8 @@ // Author: fengshengxiong // Date: 2024/5/8 -// Description: 收藏数据 +// Description: 收藏音频盒子 +import 'package:hive/hive.dart'; import 'package:tone_snap/data/models/voice_model.dart'; import 'package:tone_snap/data/storage/hive_storage.dart'; @@ -19,7 +20,7 @@ class FavoriteBox { /// 声明盒子 /// 注意, main函数中这个盒子已经打开, 可以进行存储操作 - final _box = getFavoriteBox(); + final _box = Hive.box(favoriteBox); /// 获取数据 List getList() { diff --git a/lib/data/storage/hive_storage.dart b/lib/data/storage/hive_storage.dart index 08164d2..935f6dd 100644 --- a/lib/data/storage/hive_storage.dart +++ b/lib/data/storage/hive_storage.dart @@ -5,12 +5,16 @@ import 'package:hive_flutter/hive_flutter.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/playlist_model.dart'; import 'package:tone_snap/data/models/voice_model.dart'; const myVoiceBox = 'myVoiceBox'; const favoriteBox = 'favoriteBox'; const musicBox = 'musicBox'; const loveSongsBox = 'loveSongsBox'; +const offlineBox = 'offlineBox'; +const playlistsBox = 'playlistsBox'; +const collectPlaylistsBox = 'collectPlaylistsBox'; Future initHive() async { // 初始化 @@ -19,25 +23,14 @@ Future initHive() async { Hive.registerAdapter(VoiceModelAdapter()); Hive.registerAdapter(PlayModeAdapter()); Hive.registerAdapter(MusicModelAdapter()); + Hive.registerAdapter(PlaylistModelAdapter()); + // 打开盒子 await Hive.openBox(myVoiceBox); await Hive.openBox(favoriteBox); await Hive.openBox(musicBox); await Hive.openBox(loveSongsBox); -} - -Box getMyVoiceBox() { - return Hive.box(myVoiceBox); -} - -Box getFavoriteBox() { - return Hive.box(favoriteBox); -} - -Box getMusicBox() { - return Hive.box(musicBox); -} - -Box getLoveSongsBox() { - return Hive.box(loveSongsBox); + await Hive.openBox(offlineBox); + await Hive.openBox(playlistsBox); + await Hive.openBox(collectPlaylistsBox); } \ No newline at end of file diff --git a/lib/data/storage/love_songs_box.dart b/lib/data/storage/love_songs_box.dart index 5035212..b1f1bf5 100644 --- a/lib/data/storage/love_songs_box.dart +++ b/lib/data/storage/love_songs_box.dart @@ -1,8 +1,9 @@ // Author: fengshengxiong // Date: 2024/5/8 -// Description: 喜欢的歌曲 +// Description: 收藏歌曲盒子 import 'package:get/get.dart'; +import 'package:hive/hive.dart'; import 'package:tone_snap/data/models/music_model.dart'; import 'package:tone_snap/data/storage/hive_storage.dart'; @@ -17,22 +18,21 @@ class LoveSongsBox { /// 声明盒子 /// 注意, main函数中这个盒子已经打开, 可以进行存储操作 - final _box = getLoveSongsBox(); + final _box = Hive.box(loveSongsBox); - /// 获取数据 + /// 获取列表 List getList() { return _box.values.toList(); } - /// 添加数据 - Future addData(MusicModel model) async { - return await _box.add(model); + /// 获取倒序列表 + List getReversedList() { + return _box.values.toList().reversed.toList(); } - /// 校验该歌曲是否加入喜欢 - bool isLove(String videoId) { - var model = getList().firstWhereOrNull((e) => e.videoId == videoId); - return model != null; + /// 添加数据 + Future add(MusicModel model) async { + return await _box.add(model); } /// 删除 @@ -40,7 +40,7 @@ class LoveSongsBox { var list = getList(); var model = list.firstWhereOrNull((e) => e.videoId == videoId); if (model != null) { - await _box.delete(list.indexOf(model)); + await _box.deleteAt(list.indexOf(model)); await _box.flush(); } } @@ -50,4 +50,18 @@ class LoveSongsBox { await _box.clear(); await _box.flush(); } + + /// 获取第一条的封面 + String? getFirstCoverUrl() { + if (getReversedList().isNotEmpty) { + return getReversedList().first.coverUrl; + } + return null; + } + + /// 校验该歌曲是否收藏 + bool checkLove(String? videoId) { + var model = getList().firstWhereOrNull((e) => e.videoId == videoId); + return model != null; + } } diff --git a/lib/data/storage/music_box.dart b/lib/data/storage/music_box.dart index 1072d29..655d5a4 100644 --- a/lib/data/storage/music_box.dart +++ b/lib/data/storage/music_box.dart @@ -1,9 +1,16 @@ // Author: fengshengxiong // Date: 2024/5/8 -// Description: 音乐数据盒子 +// Description: 音乐非结构化数据盒子 +import 'dart:convert'; + +import 'package:hive/hive.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:tone_snap/data/enum/play_mode.dart'; import 'package:tone_snap/data/storage/hive_storage.dart'; +import 'package:tone_snap/global/app_config.dart'; +import 'package:tone_snap/utils/log_util.dart'; +import 'package:tone_snap/utils/obj_util.dart'; class MusicBox { MusicBox._(); @@ -14,17 +21,159 @@ class MusicBox { return _instance; } + /// RemoteConfig openStatus + final _openStatusKey = 'openStatusKey'; + + /// RemoteConfig dataVersion + final _dataVersionKey = 'dataVersionKey'; + + /// 是否进入过B面 + final _isOpenedSideBKey = 'isOpenedSideBKey'; + + /// 开屏启动插页事件间隔时长 + final _openAppEventDurationKey = 'openAppEventDurationKey'; + + /// 插页广告事件间隔时长 + final _interstitialEventDurationKey = 'interstitialEventDurationKey'; + + /// 播放模式 + final _playModeKey = 'playModeKey'; + + /// 搜索历史 + final _searchHistoryKey = 'searchHistoryKey'; + /// 声明盒子 /// 注意, main函数中这个盒子已经打开, 可以进行存储操作 - final _box = getMusicBox(); + final _box = Hive.box(musicBox); + + /// 设置 openStatus + Future putOpenStatus(String openStatus) async { + return await _box.put(_openStatusKey, openStatus); + } + + /// 获取远程版本 + Future getVersionCode() async { + String? openStatus = _box.get(_openStatusKey); + String? versionCode; + if (ObjUtil.isNotEmpty(openStatus)) { + try { + versionCode = jsonDecode(openStatus!)['versionCode']; + } catch (e) { + LogUtil.e(e.toString()); + } + } + final packageInfo = await PackageInfo.fromPlatform(); + return ObjUtil.isEmpty(versionCode) ? packageInfo.version : versionCode!; + } + + /// 获取开关 + bool getEnter() { + String? openStatus = _box.get(_openStatusKey); + if (ObjUtil.isNotEmpty(openStatus)) { + try { + return jsonDecode(openStatus!)['enter'] ?? false; + } catch (e) { + LogUtil.e(e.toString()); + } + } + return false; + } + + /// 设置 dataVersion + Future putDataVersion(String dataVersion) async { + return await _box.put(_dataVersionKey, dataVersion); + } + + /// 获取 ClientVersion + String getClientVersion() { + String? dataVersion = _box.get(_dataVersionKey); + if (ObjUtil.isNotEmpty(dataVersion)) { + try { + if (ObjUtil.isNotEmpty(jsonDecode(dataVersion!)['ClientVersion'])) { + return jsonDecode(dataVersion)['ClientVersion']; + } + } catch (e) { + LogUtil.e(e.toString()); + } + } + return AppConfig.clientVersion; + } + + /// 获取 PlayerVersion + String getPlayerVersion() { + String? dataVersion = _box.get(_dataVersionKey); + if (ObjUtil.isNotEmpty(dataVersion)) { + try { + if (ObjUtil.isNotEmpty(jsonDecode(dataVersion!)['PlayerVersion'])) { + return jsonDecode(dataVersion)['PlayerVersion']; + } + } catch (e) { + LogUtil.e(e.toString()); + } + } + return AppConfig.playerVersion; + } + + /// 设置是否打开过B面 + Future putIsOpenedSideB(bool isOpen) async { + return await _box.put(_isOpenedSideBKey, isOpen); + } + + /// 获取是否打开过B面 + bool getIsOpenedSideB() { + return _box.get(_isOpenedSideBKey, defaultValue: false); + } + + /// 设置开屏启动插页事件间隔时长 + Future putOpenAppEventDuration(int time) async { + return await _box.put(_openAppEventDurationKey, time); + } + + /// 获取开屏启动插页事件间隔时长 + int getOpenAppEventDuration() { + return _box.get(_openAppEventDurationKey, defaultValue: AppConfig.openAppEventDurationTime); + } + + /// 设置插页广告事件间隔时长 + Future putInterstitialEventDuration(int time) async { + return await _box.put(_interstitialEventDurationKey, time); + } + + /// 获取插页广告事件间隔时长 + int getInterstitialEventDuration() { + return _box.get(_interstitialEventDurationKey, defaultValue: AppConfig.interstitialEventDuration); + } /// 设置播放模式 - Future putPlayMode(PlayMode playMode) { - return _box.put('play_mode', playMode); + Future putPlayMode(PlayMode playMode) async { + return await _box.put(_playModeKey, playMode); } /// 获取播放模式 PlayMode getPlayMode() { - return _box.get('play_mode') ?? PlayMode.listLoop; + return _box.get(_playModeKey) ?? PlayMode.listLoop; + } + + /// 添加搜索历史 + Future putSearchHistory(String history) async { + var historyList = getAllSearchHistory(); + if (!historyList.contains(history)) { + if (historyList.length >= 9) { + historyList.removeLast(); + } + historyList.insert(0, history); + await _box.put(_searchHistoryKey, historyList); + } + } + + /// 获取所有搜索历史 + List getAllSearchHistory() { + return _box.get(_searchHistoryKey, defaultValue: []); + } + + /// 删除所有搜索历史 + Future deleteAllSearchHistory() async { + await _box.delete(_searchHistoryKey); + await _box.flush(); } } diff --git a/lib/data/storage/my_voice_box.dart b/lib/data/storage/my_voice_box.dart index 9ea698f..b93b9a8 100644 --- a/lib/data/storage/my_voice_box.dart +++ b/lib/data/storage/my_voice_box.dart @@ -1,7 +1,8 @@ // Author: fengshengxiong // Date: 2024/5/8 -// Description: 我的音频 +// Description: 我的音频盒子 +import 'package:hive/hive.dart'; import 'package:tone_snap/data/models/voice_model.dart'; import 'package:tone_snap/data/storage/hive_storage.dart'; @@ -19,7 +20,7 @@ class MyVoiceBox { /// 声明盒子 /// 注意, main函数中这个盒子已经打开, 可以进行存储操作 - final _box = getMyVoiceBox(); + final _box = Hive.box(myVoiceBox); /// 获取数据 List getList() { diff --git a/lib/data/storage/offline_box.dart b/lib/data/storage/offline_box.dart new file mode 100644 index 0000000..6445023 --- /dev/null +++ b/lib/data/storage/offline_box.dart @@ -0,0 +1,67 @@ +// Author: fengshengxiong +// Date: 2024/5/8 +// Description: 下载歌曲盒子 + +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/storage/hive_storage.dart'; + +class OfflineBox { + OfflineBox._(); + + static final OfflineBox _instance = OfflineBox._(); + + factory OfflineBox() { + return _instance; + } + + /// 声明盒子 + /// 注意, main函数中这个盒子已经打开, 可以进行存储操作 + final _box = Hive.box(offlineBox); + + /// 获取列表 + List getList() { + return _box.values.toList(); + } + + /// 获取倒序列表 + List getReversedList() { + return _box.values.toList().reversed.toList(); + } + + /// 添加数据 + Future add(MusicModel model) async { + return await _box.add(model); + } + + /// 删除 + Future delete(String videoId) async { + var list = getList(); + var model = list.firstWhereOrNull((e) => e.videoId == videoId); + if (model != null) { + await _box.deleteAt(list.indexOf(model)); + await _box.flush(); + } + } + + /// 清空所有数据 + Future clear() async { + await _box.clear(); + await _box.flush(); + } + + /// 获取第一条的封面 + String? getFirstCoverUrl() { + if (getReversedList().isNotEmpty) { + return getReversedList().first.coverUrl; + } + return null; + } + + /// 校验是否已下载 + bool checkDownloaded(String? videoId) { + var model = getList().firstWhereOrNull((e) => e.videoId == videoId); + return model != null; + } +} diff --git a/lib/data/storage/playlists_box.dart b/lib/data/storage/playlists_box.dart new file mode 100644 index 0000000..4f928dc --- /dev/null +++ b/lib/data/storage/playlists_box.dart @@ -0,0 +1,117 @@ +// Author: fengshengxiong +// Date: 2024/5/8 +// Description: 自定义播放列表盒子 + +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/models/playlist_model.dart'; +import 'package:tone_snap/data/storage/hive_storage.dart'; +import 'package:tone_snap/utils/date_util.dart'; + +class PlaylistsBox { + PlaylistsBox._(); + + static final PlaylistsBox _instance = PlaylistsBox._(); + + factory PlaylistsBox() { + return _instance; + } + + /// 声明盒子 + /// 注意, main函数中这个盒子已经打开, 可以进行存储操作 + final _box = Hive.box(playlistsBox); + + /// 获取列表 + List getList() { + return _box.values.toList(); + } + + /// 获取倒序列表 + List getReversedList() { + return _box.values.toList().reversed.toList(); + } + + /// 添加数据 + Future add(String title) async { + final milliseconds = DateUtil.getNowTimestamp(); + final id = '$title-$milliseconds'; + await _box.put(id, PlaylistModel( + id: id, + title: title, + milliseconds: milliseconds, + musicList: [], + )); + } + + /// 删除 + Future delete(String id) async { + var list = getList(); + final playlistModel = list.firstWhereOrNull((e) => e.id == id); + if (playlistModel != null) { + await _box.deleteAt(list.indexOf(playlistModel)); + await _box.flush(); + } + } + + /// 清空所有数据 + Future clear() async { + await _box.clear(); + await _box.flush(); + } + + PlaylistModel? getPlaylistModel(String id) { + return _box.get(id); + } + + /// 编辑 + void editTitle(String id, String title) { + var list = getList(); + final playlistModel = list.firstWhereOrNull((e) => e.id == id); + if (playlistModel != null) { + playlistModel.title = title; + _box.put(id, playlistModel); + } + } + + /// 添加歌曲 + Future addMusic(String id, MusicModel musicModel) async { + final playlistModel = _box.get(id); + if (playlistModel != null) { + playlistModel.musicList ??= []; + if (playlistModel.musicList!.firstWhereOrNull((e) => e.videoId == musicModel.videoId) != null) { + BaseEasyLoading.toast('The current song is already in this playlist'); + return false; + } + playlistModel.musicList!.insert(0, musicModel); + _box.put(id, playlistModel); + return true; + } + return false; + } + + /// 删除歌曲 + Future removeMusic(String id, String videoId) async { + final playlistModel = _box.get(id); + if (playlistModel != null && playlistModel.musicList != null) { + if (playlistModel.musicList!.firstWhereOrNull((e) => e.videoId == videoId) != null) { + playlistModel.musicList!.removeWhere((e) => e.videoId == videoId); + _box.put(id, playlistModel); + return true; + } + } + return false; + } + + /// 获取当前列表的封面 + String? getFirstCoverUrl(String id) { + final playlistModel = _box.get(id); + if (playlistModel != null) { + if (playlistModel.musicList != null && playlistModel.musicList!.isNotEmpty) { + return playlistModel.musicList!.first.coverUrl; + } + } + return null; + } +} diff --git a/lib/firebase/firebase_analytics_manager.dart b/lib/firebase/firebase_analytics_manager.dart index 6e7bcc7..36acf0a 100644 --- a/lib/firebase/firebase_analytics_manager.dart +++ b/lib/firebase/firebase_analytics_manager.dart @@ -3,14 +3,9 @@ // Description: firebase_analytics管理 import 'package:firebase_analytics/firebase_analytics.dart'; -import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:flutter/foundation.dart'; class FirebaseAnalyticsManager { - /// 仅在非调试版本中启用它 - static Future setCrashlyticsCollectionEnabled() async { - await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(!kDebugMode); - } + static const homeApv = 'home_a_pv'; /// 埋点 /// name:事件名 diff --git a/lib/firebase/firebase_crashlytics_manager.dart b/lib/firebase/firebase_crashlytics_manager.dart new file mode 100644 index 0000000..053d7f5 --- /dev/null +++ b/lib/firebase/firebase_crashlytics_manager.dart @@ -0,0 +1,35 @@ +// Author: fengshengxiong +// Date: 2024/6/26 +// Description: firebase_crashlytics管理 + +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; +import 'package:tone_snap/utils/log_util.dart'; + +class FirebaseCrashlyticsManager { + static Future setEnabled() async { + await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(kReleaseMode); + } + + static void recordFlutterError() { + // 非异步错误 + FlutterError.onError = (errorDetails) { + // 插件错误 flutter_cache_manager + // https://github.com/Baseflow/flutter_cache_manager/issues/460 + if (errorDetails.exceptionAsString().contains('No host specified in URI')) { + return; + } + LogUtil.e(errorDetails.exception); + FirebaseCrashlytics.instance.recordFlutterError(errorDetails); + }; + } + + static void recordError() { + // 异步错误 + PlatformDispatcher.instance.onError = (error, stack) { + LogUtil.e(error); + FirebaseCrashlytics.instance.recordError(error, stack); + return true; + }; + } +} diff --git a/lib/firebase/firebase_remote_config_manager.dart b/lib/firebase/firebase_remote_config_manager.dart new file mode 100644 index 0000000..16adbd8 --- /dev/null +++ b/lib/firebase/firebase_remote_config_manager.dart @@ -0,0 +1,69 @@ +// Author: fengshengxiong +// Date: 2024/6/26 +// Description: firebase_remote_config管理 + +import 'dart:convert'; + +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:tone_snap/data/storage/music_box.dart'; +import 'package:tone_snap/utils/log_util.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class FirebaseRemoteConfigManager { + static Future getAll() async { + final remoteConfig = FirebaseRemoteConfig.instance; + await remoteConfig.setConfigSettings(RemoteConfigSettings( + fetchTimeout: const Duration(minutes: 1), + minimumFetchInterval: Duration.zero, + )); + bool result = await remoteConfig.fetchAndActivate(); + if (result) { + Map allData = remoteConfig.getAll(); + try { + if (allData.isNotEmpty) { + LogUtil.d('远程配置获取成功'); + + // 获取 openStatus + var openStatus = allData['openStatus']?.asString(); + if (ObjUtil.isNotEmpty(openStatus)) { + await MusicBox().putOpenStatus(openStatus!); + } + + // 获取 dataVersion + var dataVersion = allData['dataVersion']?.asString(); + if (ObjUtil.isNotEmpty(dataVersion)) { + await MusicBox().putDataVersion(dataVersion!); + } + + // 获取 openAppEventDuration + var openAppEventDuration = allData['openAppEventDuration']?.asString(); + if (ObjUtil.isNotEmpty(openAppEventDuration)) { + try { + var times = jsonDecode(openAppEventDuration!)['times']; + if (ObjUtil.isNotEmpty(times)) { + await MusicBox().putOpenAppEventDuration(times); + } + } catch (e) { + LogUtil.e(e.toString()); + } + } + + // 获取 interstitialEventDuration + var interstitialEventDuration = allData['interstitialEventDuration']?.asString(); + if (ObjUtil.isNotEmpty(interstitialEventDuration)) { + try { + var times = jsonDecode(interstitialEventDuration!)['times']; + if (ObjUtil.isNotEmpty(times)) { + await MusicBox().putInterstitialEventDuration(times); + } + } catch (e) { + LogUtil.e(e.toString()); + } + } + } + } catch (e) { + LogUtil.e(e.toString()); + } + } + } +} diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index 75bdb46..fd4f752 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -44,7 +44,7 @@ class Assets { static const String sideAHomeBg = 'assets/images/side_a/home_bg.png'; static const String sideAHomeBnbBg = 'assets/images/side_a/home_bnb_bg.png'; static const String sideAIconChevronRight = 'assets/images/side_a/icon_chevron_right.png'; - static const String sideALaunchImage = 'assets/images/side_a/launch_image.png'; + static const String sideALaunchBg = 'assets/images/side_a/launch_bg.png'; static const String sideAMore = 'assets/images/side_a/more.png'; static const String sideANotFavorite = 'assets/images/side_a/not_favorite.png'; static const String sideANotPlayed = 'assets/images/side_a/not_played.png'; @@ -63,8 +63,8 @@ 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 sideBAddToPlaylist = 'assets/images/side_b/add_to_playlist.png'; + static const String sideBAddToQueue = 'assets/images/side_b/add_to_queue.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'; @@ -77,29 +77,48 @@ class Assets { 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 sideBCollected = 'assets/images/side_b/collected.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 sideBDeleteHistory = 'assets/images/side_b/delete_history.png'; + static const String sideBDeleteWhite = 'assets/images/side_b/delete_white.png'; + static const String sideBDownloaded = 'assets/images/side_b/downloaded.png'; + static const String sideBEmpty = 'assets/images/side_b/empty.jpg'; 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 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 sideBLoveSolid = 'assets/images/side_b/love_solid.png'; + static const String sideBLoveSongsBg = 'assets/images/side_b/love_songs_bg.png'; + static const String sideBMore = 'assets/images/side_b/more.png'; + static const String sideBMoreEdit = 'assets/images/side_b/more_edit.png'; + static const String sideBMoreRemove = 'assets/images/side_b/more_remove.png'; + static const String sideBMusicBarNext = 'assets/images/side_b/music_bar_next.png'; + static const String sideBMusicPlaceholder = 'assets/images/side_b/music_placeholder.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 sideBNotDownload1 = 'assets/images/side_b/not_download1.png'; + static const String sideBNotDownload2 = 'assets/images/side_b/not_download2.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 sideBPlayListDelete = 'assets/images/side_b/play_list_delete.png'; + static const String sideBPlaylistPlayAll = 'assets/images/side_b/playlist_play_all.png'; + static const String sideBPlaylistPlayAllRandom = 'assets/images/side_b/playlist_play_all_random.png'; + static const String sideBPlaylistTitleBg = 'assets/images/side_b/playlist_title_bg.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 sideBPrivacyPolicy = 'assets/images/side_b/privacy_policy.png'; + static const String sideBReport = 'assets/images/side_b/report.png'; + static const String sideBSearch = 'assets/images/side_b/search.png'; + static const String sideBSearchWhite = 'assets/images/side_b/search_white.png'; + static const String sideBSettingBg = 'assets/images/side_b/setting_bg.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'; + static const String sideBTermsOfService = 'assets/images/side_b/terms_of_service.png'; } diff --git a/lib/global/app_config.dart b/lib/global/app_config.dart index f9cf59f..7f4e180 100644 --- a/lib/global/app_config.dart +++ b/lib/global/app_config.dart @@ -5,13 +5,24 @@ import 'package:tone_snap/data/enum/app_side_enum.dart'; class AppConfig { - static const appName = 'ToneSnap'; + static const String appName = 'ToneSnap'; /// 当前App展示的一面 - static const AppSideEnum appSideEnum = AppSideEnum.sideB; + static AppSideEnum appSideEnum = AppSideEnum.sideA; - /// 默认语言环境和所在区域 - static String defaultLocale = 'zh-CN'; + /// 开屏启动插页事件间隔时长/秒,默认值 + static const int openAppEventDurationTime = 10; + + /// 插页广告事件间隔时长/秒,默认值 + static const int interstitialEventDuration = 40; + + /// ClientVersion,默认值 + static const String clientVersion = '1.20240618.01.00'; + + /// PlayerVersion,默认值 + static const String playerVersion = '6.18.1'; + + /// 所在区域,默认值 static String isoCode = 'HK'; } diff --git a/lib/global/app_lifecycle_reactor.dart b/lib/global/app_lifecycle_reactor.dart index 290eef8..9904460 100644 --- a/lib/global/app_lifecycle_reactor.dart +++ b/lib/global/app_lifecycle_reactor.dart @@ -7,12 +7,17 @@ import 'package:tone_snap/ads/app_open_ad_manager.dart'; import 'package:tone_snap/ads/interstitial_ad_manager.dart'; class AppLifecycleReactor { - AppLifecycleReactor(); + AppLifecycleReactor._(); + + static final AppLifecycleReactor _instance = AppLifecycleReactor._(); + + factory AppLifecycleReactor() { + return _instance; + } void listenToAppStateChanges() { AppStateEventNotifier.startListening(); - AppStateEventNotifier.appStateStream - .forEach((state) => _onAppStateChanged(state)); + AppStateEventNotifier.appStateStream.forEach((state) => _onAppStateChanged(state)); } void _onAppStateChanged(AppState appState) { diff --git a/lib/global/download_manager.dart b/lib/global/download_manager.dart new file mode 100644 index 0000000..da68314 --- /dev/null +++ b/lib/global/download_manager.dart @@ -0,0 +1,165 @@ +// Author: fengshengxiong +// Date: 2024/5/10 +// Description: 下载管理 + +import 'package:background_downloader/background_downloader.dart'; +import 'package:dio/dio.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_easyloading.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/player_model.dart'; +import 'package:tone_snap/data/storage/offline_box.dart'; +import 'package:tone_snap/modules/sideb/offline/offline_controller.dart'; +import 'package:tone_snap/modules/sideb/personal_music_library/personal_music_library_controller.dart'; +import 'package:tone_snap/utils/date_util.dart'; +import 'package:tone_snap/utils/local_path_util.dart'; +import 'package:tone_snap/utils/log_util.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class DownloadManager { + static final DownloadManager _instance = DownloadManager._getInstance(); + + factory DownloadManager() => _instance; + + static MemoryTaskQueue? tq; + + List> downloadList = []; + + DownloadManager._getInstance() { + if (tq == null) { + tq ??= MemoryTaskQueue(); + tq!.maxConcurrent = 3; // no more than 5 tasks active at any one time + tq!.maxConcurrentByHost = 3; // no more than two tasks talking to the same host at the same time + tq!.maxConcurrentByGroup = 3; // no more than three tasks from the same group active at the same time + FileDownloader().addTaskQueue(tq!); // 'connects' the TaskQueue to the FileDownloader + FileDownloader().updates.listen((update) async { // listen to updates as per usual + Rx? musicModel = downloadList.firstWhereOrNull((e) => e.value.videoId == update.task.taskId); + if (musicModel == null) return; + + if (update.runtimeType == TaskStatusUpdate) { + TaskStatus taskStatus = (update as TaskStatusUpdate).status; + LogUtil.d('${update.task.filename},任务状态: $taskStatus'); + musicModel.update((fn) => fn?.taskStatus = taskStatus); + switch (taskStatus) { + case TaskStatus.enqueued: + break; + case TaskStatus.running: + break; + case TaskStatus.complete: + LogUtil.d('音乐下载路径:${await update.task.filePath()}'); + musicModel.value.localPath = await update.task.filePath(); + OfflineBox().add(musicModel.value.copyWith()); + downloadList.remove(musicModel); + BaseEasyLoading.toast('Download completed'); + if (Get.isRegistered()) { + PersonalMusicLibraryController.to.refreshOffline(); + } + if (Get.isRegistered()) { + OfflineController.to.getOfflineList(); + } + break; + case TaskStatus.notFound: + BaseEasyLoading.toast('Download failed'); + downloadList.remove(musicModel); + break; + case TaskStatus.failed: + BaseEasyLoading.toast('Download failed'); + downloadList.remove(musicModel); + break; + case TaskStatus.canceled: + BaseEasyLoading.toast('Download cancelled'); + downloadList.remove(musicModel); + break; + case TaskStatus.waitingToRetry: + break; + case TaskStatus.paused: + break; + } + } + if (update.runtimeType == TaskProgressUpdate) { + LogUtil.d('${update.task.filename},下载进度: $update'); + musicModel.update((fn) => fn?.progress = (update as TaskProgressUpdate).progress); + } + }); + } + } + + /// 下载文件 + void downloadMusic(Rx? musicModel) { + if (musicModel == null) return; + musicModel.update((fn) { + fn?.taskStatus = TaskStatus.enqueued; + fn?.cancelToken = CancelToken(); + }); + _getMusicUrl(musicModel, (url, mimeType) async { + if (ObjUtil.isEmpty(url)) { + BaseEasyLoading.toast('Resource acquisition failed'); + musicModel.update((fn) => fn?.taskStatus = TaskStatus.failed); + return; + } + String extension = mimeType ?? 'mp4'; + if (ObjUtil.isNotEmpty(mimeType)) { + // 从 mimeType 中提取主类型和子类型 + String type = mimeType!.split(';')[0].trim(); + // 获取文件扩展名 + extension = type.split('/')[1]; + } + final filename = '${musicModel.value.title}_${DateUtil.getNowTimestamp()}.$extension'; + final task = DownloadTask( + taskId: musicModel.value.videoId, + url: url!, + filename: filename, + directory: LocalPathUtil.getMusicDownloadDir(), + updates: Updates.statusAndProgress, + requiresWiFi: false, + retries: 0, + allowPause: false, + metaData: '', + ); + tq?.add(task); + downloadList.add(musicModel); + }); + } + + Future _getMusicUrl(Rx musicModel, Function(String? url, String? mimeType) onTap) async { + PlayerModel? playerModel = await MusicApi.player( + videoId: musicModel.value.videoId, + cancelToken: musicModel.value.cancelToken, + fail: (baseError) { + if (baseError.code == DioExceptionType.cancel.index) { + musicModel.update((fn) { + fn?.taskStatus = TaskStatus.canceled; + fn?.cancelToken = null; + }); + } else { + musicModel.update((fn) { + fn?.taskStatus = TaskStatus.failed; + fn?.cancelToken = null; + }); + } + } + ); + if (playerModel != null) { + if (ObjUtil.isEmpty(musicModel.value.coverUrl)) { + var thumbnails = playerModel.videoDetails?.thumbnail?.thumbnails; + if (thumbnails != null && thumbnails.isNotEmpty) { + musicModel.value.coverUrl = thumbnails[0].url; + } + } + if (ObjUtil.isEmpty(musicModel.value.musicType)) { + musicModel.value.musicType = playerModel.videoDetails?.musicVideoType; + } + var formats = playerModel.streamingData?.formats; + if (formats != null && formats.isNotEmpty) { + onTap(formats[0].url, formats[0].mimeType); + } + } + } + + void cancelDownload(Rx? musicModel) { + if (musicModel == null || musicModel.value.videoId == null) return; + musicModel.value.cancelToken?.cancel(); + FileDownloader().cancelTaskWithId(musicModel.value.videoId!); + } +} diff --git a/lib/global/download_queue_task.dart b/lib/global/download_queue_task.dart new file mode 100644 index 0000000..1950f92 --- /dev/null +++ b/lib/global/download_queue_task.dart @@ -0,0 +1,44 @@ +// Author: fengshengxiong +// Date: 2024/7/17 +// Description: 下载队列 + +import 'dart:collection'; + +class DownloadQueueTask { + int maxThread = 1; + + DownloadQueueTask(this.maxThread); + + ///当前任务队列 + final Queue<_TaskInfo> _queue = Queue(); + + ///是否工作中 + int _taskCount = 0; + + void create(String taskName, Function(String name) workTask) { + _queue.add(_TaskInfo(taskName, workTask)); + _exec(); + } + + void _exec() async { + if (_taskCount >= maxThread) return; + if (_queue.isEmpty) return; + + for (int i = 0; i < maxThread; i++) { + if (_queue.isEmpty) continue; + + _TaskInfo taskInfo = _queue.removeFirst(); + _taskCount += 1; + await taskInfo.workTask.call(taskInfo.taskName); + _taskCount -= 1; + } + _exec(); + } +} + +class _TaskInfo { + String taskName; + Function(String taskName) workTask; + + _TaskInfo(this.taskName, this.workTask); +} \ No newline at end of file diff --git a/lib/global/network_connectivity_service.dart b/lib/global/network_connectivity_service.dart index 0c9cd05..446baf5 100644 --- a/lib/global/network_connectivity_service.dart +++ b/lib/global/network_connectivity_service.dart @@ -8,22 +8,34 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:get/get.dart'; import 'package:tone_snap/ads/app_open_ad_manager.dart'; import 'package:tone_snap/ads/interstitial_ad_manager.dart'; +import 'package:tone_snap/firebase/firebase_remote_config_manager.dart'; +import 'package:tone_snap/modules/launch/launch_controller.dart'; import 'package:tone_snap/utils/log_util.dart'; class NetworkConnectivityService extends GetxService { StreamSubscription>? subscription; - List recordResult = []; + /// 是否执行过以下任务,任务只执行一次 + var isExecutedTask = false; @override void onInit() { super.onInit(); subscription = Connectivity().onConnectivityChanged.listen((List result) { - LogUtil.d('网络连接类型变化:$result'); - if (recordResult.contains(ConnectivityResult.none) && !result.contains(ConnectivityResult.none)) { - AppOpenAdManager().loadAd(); - InterstitialAdManager().loadAd(); + LogUtil.d('当前网络连接类型:$result'); + if (result.contains(ConnectivityResult.wifi) || result.contains(ConnectivityResult.mobile)) { + if (!isExecutedTask) { + isExecutedTask = true; + + FirebaseRemoteConfigManager.getAll(); + + if (Get.isRegistered()) { + LaunchController.to.getIsoCode(); + } + + InterstitialAdManager().loadAd(); + AppOpenAdManager().loadAd(); + } } - recordResult = result; }); } diff --git a/lib/main.dart b/lib/main.dart index 88291ce..1c579f5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,23 +1,22 @@ import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; 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/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_crashlytics_manager.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/main_controller.dart'; import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart'; +import 'package:tone_snap/modules/sideb/music_bar/music_bar.dart'; +import 'package:tone_snap/modules/sideb/music_bar/music_bar_controller.dart'; import 'package:tone_snap/res/themes/app_themes.dart'; import 'package:tone_snap/routes/app_pages.dart'; import 'package:tone_snap/routes/app_routes.dart'; @@ -28,34 +27,19 @@ import 'package:tone_snap/utils/log_util.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - 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()); + // 初始化Firebase + try { + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await FirebaseCrashlyticsManager.setEnabled(); + FirebaseCrashlyticsManager.recordFlutterError(); + FirebaseCrashlyticsManager.recordError(); + } catch (e) { + LogUtil.e("Firebase initialization error: $e"); } + // 初始化广告 SDK + MobileAds.instance.initialize(); + // 初始化Hive await initHive(); @@ -87,53 +71,62 @@ 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(); + navigatorObservers = [ + GetObserver((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (Get.currentRoute == AppRoutes.playPage) { + MusicBar().hide(); + } else { + if (Get.isRegistered()) { + if (MusicPlayerController.to.getMusicModel()?.value.videoId != null) { + if (Get.isBottomSheetOpen != null && Get.isBottomSheetOpen!) { + MusicBar().hide(); + } else { + MusicBar().show(); + } + } + } + } + if (Get.isRegistered()) { + if (Get.currentRoute == AppRoutes.initialB) { + MusicBarController.to.riseUp(); } else { - if (Get.isRegistered() && MusicPlayerController.to.musicModel.value.videoId != null) { - MusicBar().show(); - } + MusicBarController.to.toBottom(); } - 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) { - 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); - }, + return KeyboardDismissOnTap( + dismissOnCapturedTaps: true, + child: GetBuilder( + id: 'changeTheme', + init: MainController(), + builder: (logic) { + return GetMaterialApp( + title: AppConfig.appName, + debugShowCheckedModeBanner: false, + theme: logic.isSideBTheme.value ? sideBTheme : sideATheme, + darkTheme: logic.isSideBTheme.value ? sideBTheme : sideATheme, + themeMode: ThemeMode.dark, + initialRoute: AppRoutes.launch, + getPages: AppPages.routes, + navigatorObservers: navigatorObservers, + builder: (context, widget) { + BaseEasyLoading.configLoading(); + widget = easyLoading(context, widget); + // 设置文字大小不随系统设置改变 + return MediaQuery.withNoTextScaling(child: widget); + }, + ); + }, + ), ); }, ); diff --git a/lib/modules/launch/launch_binding.dart b/lib/modules/launch/launch_binding.dart new file mode 100644 index 0000000..82deb40 --- /dev/null +++ b/lib/modules/launch/launch_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/launch/launch_controller.dart'; + +class LaunchBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => LaunchController()); + } +} diff --git a/lib/modules/launch/launch_controller.dart b/lib/modules/launch/launch_controller.dart new file mode 100644 index 0000000..802b430 --- /dev/null +++ b/lib/modules/launch/launch_controller.dart @@ -0,0 +1,125 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:tone_snap/ads/app_open_ad_manager.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/models/isocode_model.dart'; +import 'package:tone_snap/data/storage/music_box.dart'; +import 'package:tone_snap/global/app_config.dart'; +import 'package:tone_snap/global/app_lifecycle_reactor.dart'; +import 'package:tone_snap/global/network_connectivity_service.dart'; +import 'package:tone_snap/modules/sideb/controllers/main_controller.dart'; +import 'package:tone_snap/res/themes/app_themes.dart'; +import 'package:tone_snap/routes/app_routes.dart'; +import 'package:tone_snap/utils/log_util.dart'; + +class LaunchController extends GetxController with GetSingleTickerProviderStateMixin { + static LaunchController get to => Get.find(); + Timer? _timer; + /// 进度总时长 + var timeTotal = 10 * 1000; + /// 当前进度 + var currentProcess = 0.obs; + /// 进度每次变化值 + var changeValue = 10; + + late AppLifecycleReactor _appLifecycleReactor; + + @override + void onInit() { + super.onInit(); + _appLifecycleReactor = AppLifecycleReactor(); + _appLifecycleReactor.listenToAppStateChanges(); + + // 监听网络变化 + Get.putAsync(() async => NetworkConnectivityService()); + + _startTimer(); + } + + @override + void onClose() { + _stopTimer(); + super.onClose(); + } + + /// 开始定时器 + void _startTimer() { + _timer = Timer.periodic(Duration(milliseconds: changeValue), (Timer t) { + if (currentProcess.value + changeValue >= timeTotal) { + currentProcess.value = timeTotal; + if (currentProcess >= timeTotal) { + _stopTimer(); + _checkEnter(); + return; + } + } + currentProcess.value += changeValue; + }); + } + + /// 停止定时器 + void _stopTimer() { + _timer?.cancel(); + _timer = null; + } + + /// 修改进度变化值 + void editChangeValue() { + changeValue = 3000; + } + + /// 校验开关和版本,决定进A还是B + Future _checkEnter() async { + bool isOpenedSideB = MusicBox().getIsOpenedSideB(); + if (isOpenedSideB) { + LogUtil.d('进入过B面'); + _openSideB(); + } else { + bool enter = MusicBox().getEnter(); + String versionCode = await MusicBox().getVersionCode(); + final packageInfo = await PackageInfo.fromPlatform(); + if (versionCode != packageInfo.version) { + LogUtil.d('版本不相同,进入B面'); + _openSideB(); + } else { + if (enter) { + LogUtil.d('开关:打开'); + _openSideB(); + } else { + LogUtil.d('开关:关闭'); + _openSideA(); + } + } + } + } + + void _openSideA() { + AppOpenAdManager().showAdIfAvailable(onTap: () { + AppConfig.appSideEnum = AppSideEnum.sideA; + Get.offNamed(AppRoutes.initialA); + }); + } + + void _openSideB() { + AppOpenAdManager().showAdIfAvailable(onTap: () { + AppConfig.appSideEnum = AppSideEnum.sideB; + MainController.to.changeTheme(); + Get.offNamed(AppRoutes.initialB); + MusicBox().putIsOpenedSideB(true); + }); + } + + /// 获取所在区域 + Future getIsoCode() async { + BaseModel? model = await TikUsTokApi.getIsoCode(); + if (model != null && model.success && model.data?.isoCode != null) { + AppConfig.isoCode = model.data!.isoCode!; + } + } +} diff --git a/lib/modules/splash/splash_view.dart b/lib/modules/launch/launch_view.dart similarity index 52% rename from lib/modules/splash/splash_view.dart rename to lib/modules/launch/launch_view.dart index e98f7e9..435b700 100644 --- a/lib/modules/splash/splash_view.dart +++ b/lib/modules/launch/launch_view.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:step_progress_indicator/step_progress_indicator.dart'; import 'package:tone_snap/generated/assets.dart'; -import 'package:tone_snap/modules/splash/splash_controller.dart'; +import 'package:tone_snap/modules/launch/launch_controller.dart'; import 'package:tone_snap/res/themes/app_colors.dart'; -class SplashView extends StatelessWidget { - SplashView({super.key}); +class LaunchView extends StatelessWidget { + LaunchView({super.key}); - final controller = Get.find(); + final controller = Get.find(); @override Widget build(BuildContext context) { @@ -24,7 +25,7 @@ class SplashView extends StatelessWidget { Widget _buildImageBg() { return Image.asset( - Assets.sideALaunchImage, + Assets.sideALaunchBg, width: 1.sw, height: 1.sh, fit: BoxFit.cover, @@ -39,17 +40,31 @@ class SplashView extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ SizedBox( - width: 0.5.sw, + width: 0.6.sw, child: Obx(() { - return LinearProgressIndicator( - value: controller.processValue.value, - backgroundColor: Colors.white, - valueColor: const AlwaysStoppedAnimation(seedColor), + return ClipRRect( borderRadius: BorderRadius.circular(8).r, + child: StepProgressIndicator( + totalSteps: controller.timeTotal, + currentStep: controller.currentProcess.value, + size: 6, + padding: 0, + unselectedColor: Colors.white, + roundedEdges: const Radius.circular(8).r, + selectedGradientColor: const LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Color(0xffAC42FF), + Color(0xff5738D3), + Color(0xffC1ED02), + ], + ), + ), ); }), ), - SizedBox(height: 14.h), + SizedBox(height: 10.h), Text( 'Resource Loading...', style: TextStyle( diff --git a/lib/modules/sidea/change_voice/change_voice_controller.dart b/lib/modules/sidea/change_voice/change_voice_controller.dart index dc4600e..c7f1d56 100644 --- a/lib/modules/sidea/change_voice/change_voice_controller.dart +++ b/lib/modules/sidea/change_voice/change_voice_controller.dart @@ -1,5 +1,3 @@ -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'; @@ -48,9 +46,7 @@ class ChangeVoiceController extends GetxController { @override void onInit() { super.onInit(); - if (Platform.isIOS) { - InterstitialAdManager().loadAd(); - } + InterstitialAdManager().loadAd(); filePath = Get.arguments; // playerController.setFilePath(filePath); @@ -108,122 +104,65 @@ class ChangeVoiceController extends GetxController { /// 保存 Future save() async { - if (Platform.isIOS) { - // 显示插页广告 - InterstitialAdManager().showAdIfAvailable( - onTap: () async { - // 停止播放 - if (playerController.isPlaying.value) playerController.stopPlay(); - BaseEasyLoading.loading(); + // 显示插页广告 + InterstitialAdManager().showAdIfAvailable( + onTap: () async { + // 停止播放 + 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'; + + 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'); } - - // 输出目录 - 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) { + } else { + LogUtil.d('Audio processing failed'); BaseEasyLoading.toast('Audio processing failed'); } - }, - ); - } else { - // 停止播放 - if (playerController.isPlaying.value) playerController.stopPlay(); - BaseEasyLoading.loading(); - - 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'; - - 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'); + } 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 3053a68..697320b 100644 --- a/lib/modules/sidea/favourite/favourite_controller.dart +++ b/lib/modules/sidea/favourite/favourite_controller.dart @@ -57,7 +57,6 @@ class FavouriteController extends GetxController { void onTapDelete(VoiceModel item) { Get.dialog( - barrierDismissible: false, RemindDialog( content: 'Are you sure to delete it?', confirmOnTap: () async { diff --git a/lib/modules/sidea/home/home_binding.dart b/lib/modules/sidea/home/home_binding.dart deleted file mode 100644 index 08cdc76..0000000 --- a/lib/modules/sidea/home/home_binding.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:get/get.dart'; -import 'package:tone_snap/modules/sidea/home/home_controller.dart'; - -class HomeBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => HomeController()); - } -} diff --git a/lib/modules/sidea/home/home_view.dart b/lib/modules/sidea/home/home_view.dart index 2d5d724..ac5fa7c 100644 --- a/lib/modules/sidea/home/home_view.dart +++ b/lib/modules/sidea/home/home_view.dart @@ -14,7 +14,8 @@ class HomeView extends GetView { @override Widget build(BuildContext context) { - Get.find(); + Get.put(HomeController()); + return Column( children: [ HeadLabel( @@ -71,7 +72,7 @@ class HomeView extends GetView { clipBehavior: Clip.none, children: [ Visibility( - visible: ObjUtil.isNotEmptyStr(item.cover), + visible: ObjUtil.isNotEmpty(item.cover), child: ClipPath( clipper: CircularNotchClipper( notchRadius: 20, diff --git a/lib/modules/sidea/initial/initial_controller.dart b/lib/modules/sidea/initial/initial_controller.dart index 65722a8..0c8330f 100644 --- a/lib/modules/sidea/initial/initial_controller.dart +++ b/lib/modules/sidea/initial/initial_controller.dart @@ -1,12 +1,10 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:get/get.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/global/app_tracking_transparency_manager.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'; @@ -28,25 +26,20 @@ class InitialController extends GetxController { var currentIndex = 0.obs; Rx currentPlayVoiceModel = Rx(null); - /// 是否加入喜欢列表 + /// 是否收藏 var isFavourite = false.obs; - late AppLifecycleReactor _appLifecycleReactor; - @override void onInit() { super.onInit(); - // _appLifecycleReactor = AppLifecycleReactor(); - // _appLifecycleReactor.listenToAppStateChanges(); - + AppTrackingTransparencyManager().requestATT(); pageController = PageController(initialPage: currentIndex.value); } @override void onReady() async { super.onReady(); - // await FirebaseAnalyticsManager.setCrashlyticsCollectionEnabled(); - // _addEventLog(); + _addEventLog(); } @override @@ -60,9 +53,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); @@ -105,7 +98,7 @@ class InitialController extends GetxController { /// 埋点 void _addEventLog() { - FirebaseAnalyticsManager.logEvent('home_a_pv'); + FirebaseAnalyticsManager.logEvent(FirebaseAnalyticsManager.homeApv); } } diff --git a/lib/modules/sidea/me/me_binding.dart b/lib/modules/sidea/me/me_binding.dart deleted file mode 100644 index 869f903..0000000 --- a/lib/modules/sidea/me/me_binding.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:get/get.dart'; -import 'package:tone_snap/modules/sidea/me/me_controller.dart'; - -class MeBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => MeController()); - } -} diff --git a/lib/modules/sidea/me/me_view.dart b/lib/modules/sidea/me/me_view.dart index 112e403..1413f30 100644 --- a/lib/modules/sidea/me/me_view.dart +++ b/lib/modules/sidea/me/me_view.dart @@ -12,7 +12,8 @@ class MeView extends GetView { @override Widget build(BuildContext context) { - Get.find(); + Get.put(MeController()); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/modules/sidea/my_voice/my_voice_controller.dart b/lib/modules/sidea/my_voice/my_voice_controller.dart index 1e7660f..c9567b0 100644 --- a/lib/modules/sidea/my_voice/my_voice_controller.dart +++ b/lib/modules/sidea/my_voice/my_voice_controller.dart @@ -57,7 +57,6 @@ class MyVoiceController extends GetxController { void onTapDelete(VoiceModel item) { Get.dialog( - barrierDismissible: false, RemindDialog( content: 'Are you sure to delete it?', confirmOnTap: () async { diff --git a/lib/modules/sidea/play_sound/play_sound_view.dart b/lib/modules/sidea/play_sound/play_sound_view.dart index 52e3028..8badfcb 100644 --- a/lib/modules/sidea/play_sound/play_sound_view.dart +++ b/lib/modules/sidea/play_sound/play_sound_view.dart @@ -69,7 +69,7 @@ class PlaySoundView extends StatelessWidget { Widget _buildCover() { return Expanded( child: Visibility( - visible: ObjUtil.isNotEmptyStr(controller.voiceModel.cover), + visible: ObjUtil.isNotEmpty(controller.voiceModel.cover), replacement: SizedBox( width: 296.w, height: 296.w, diff --git a/lib/modules/sidea/settings/settings_view.dart b/lib/modules/sidea/settings/settings_view.dart index 49cbde5..6dbaaa0 100644 --- a/lib/modules/sidea/settings/settings_view.dart +++ b/lib/modules/sidea/settings/settings_view.dart @@ -10,7 +10,8 @@ class SettingsView extends GetView{ @override Widget build(BuildContext context) { - Get.find(); + Get.put(SettingsController()); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/modules/sidea/widgets/my_voice_item.dart b/lib/modules/sidea/widgets/my_voice_item.dart index ce6d863..6904fe2 100644 --- a/lib/modules/sidea/widgets/my_voice_item.dart +++ b/lib/modules/sidea/widgets/my_voice_item.dart @@ -33,7 +33,7 @@ class MyVoiceItem extends StatelessWidget { children: [ ClipOval( child: Image.asset( - ObjUtil.isNotEmptyStr(item.cover) ? item.cover! : Assets.sideAVoiceDefault, + ObjUtil.isNotEmpty(item.cover) ? item.cover! : Assets.sideAVoiceDefault, width: 52.w, height: 52.w, fit: BoxFit.cover, diff --git a/lib/modules/sideb/add_to_playlist_bottom_sheet/add_to_playlist_bottom_sheet_controller.dart b/lib/modules/sideb/add_to_playlist_bottom_sheet/add_to_playlist_bottom_sheet_controller.dart new file mode 100644 index 0000000..7824d78 --- /dev/null +++ b/lib/modules/sideb/add_to_playlist_bottom_sheet/add_to_playlist_bottom_sheet_controller.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/components/dialog/add_playlist_dialog.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/models/playlist_model.dart'; +import 'package:tone_snap/data/storage/playlists_box.dart'; +import 'package:tone_snap/modules/sideb/playlists/playlists_controller.dart'; + +class AddToPlaylistBottomSheetController extends GetxController { + late MusicModel musicModel; + + var playlists = [].obs; + + @override + void onReady() { + super.onReady(); + _getList(); + } + + void _getList() { + playlists.value = PlaylistsBox().getReversedList(); + } + + void setMusicModel(MusicModel musicModel) { + this.musicModel = musicModel; + } + + void createPlayList() { + Get.dialog( + barrierDismissible: false, + CreatePlaylistDialog( + onTap: (value) async { + await PlaylistsBox().add(value); + _getList(); + if (Get.isRegistered()) { + PlaylistsController.to.getList(); + } + }, + ), + ); + } + + void onTapItem(PlaylistModel playlistModel) async { + bool result = await PlaylistsBox().addMusic(playlistModel.id, musicModel); + if (result) { + BaseEasyLoading.toast('Added to playlist'); + Get.back(); + if (Get.isRegistered()) { + PlaylistsController.to.getList(); + } + } + } +} diff --git a/lib/modules/sideb/add_to_playlist_bottom_sheet/add_to_playlist_bottom_sheet_view.dart b/lib/modules/sideb/add_to_playlist_bottom_sheet/add_to_playlist_bottom_sheet_view.dart new file mode 100644 index 0000000..126c903 --- /dev/null +++ b/lib/modules/sideb/add_to_playlist_bottom_sheet/add_to_playlist_bottom_sheet_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/base_scrollbar.dart'; +import 'package:tone_snap/components/get_bind_widget.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/modules/sideb/add_to_playlist_bottom_sheet/add_to_playlist_bottom_sheet_controller.dart'; +import 'package:tone_snap/modules/sideb/widgets/playlist_item.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; + +class AddToPlaylistBottomSheetView extends StatelessWidget { + AddToPlaylistBottomSheetView({super.key, required this.musicModel}); + + final controller = Get.put(AddToPlaylistBottomSheetController()); + final MusicModel musicModel; + + @override + Widget build(BuildContext context) { + controller.setMusicModel(musicModel); + return GetBindWidget( + bind: controller, + child: Container( + width: 1.sw, + height: 0.6.sh, + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), + decoration: const BoxDecoration( + color: Color(0xFF282A2C), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(18), topRight: Radius.circular(18), + ), + ), + child: Column( + children: [ + _buildIndicator(), + _buildCreatePlaylist(), + _buildList(), + ], + ), + ), + ); + } + + Widget _buildIndicator() { + return Container( + width: 28, + height: 4, + margin: const EdgeInsets.symmetric(vertical: 8).h, + decoration: BoxDecoration( + color: const Color(0xFF555555), + borderRadius: BorderRadius.circular(20), + ), + ); + } + + Widget _buildCreatePlaylist() { + return GestureDetector( + onTap: controller.createPlayList, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 10).h, + padding: EdgeInsets.symmetric(vertical: 10.w, horizontal: 16.h), + decoration: BoxDecoration( + color: seedColor, + borderRadius: BorderRadius.circular(8).r, + ), + child: Text( + 'Create Playlist', + style: TextStyle( + color: Colors.black, + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } + + Widget _buildList() { + return Expanded( + child: BaseScrollbar( + child: Obx(() { + return ListView.builder( + itemCount: controller.playlists.length, + itemBuilder: (context, index) { + return PlaylistItem( + index: 0, + playlistModel: controller.playlists[index], + onTapItem: () => controller.onTapItem(controller.playlists[index]), + ); + }, + ); + }), + ), + ); + } +} diff --git a/lib/modules/sideb/album/album_binding.dart b/lib/modules/sideb/album/album_binding.dart deleted file mode 100644 index 06be2c0..0000000 --- a/lib/modules/sideb/album/album_binding.dart +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index b8295cb..0000000 --- a/lib/modules/sideb/album/album_controller.dart +++ /dev/null @@ -1,104 +0,0 @@ -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 deleted file mode 100644 index 7c254a8..0000000 --- a/lib/modules/sideb/album/album_view.dart +++ /dev/null @@ -1,229 +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/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/album_song_list/album_song_list_binding.dart b/lib/modules/sideb/album_song_list/album_song_list_binding.dart new file mode 100644 index 0000000..350f37a --- /dev/null +++ b/lib/modules/sideb/album_song_list/album_song_list_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/sideb/album_song_list/album_song_list_controller.dart'; + +class AlbumSongListBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => AlbumSongListController()); + } +} diff --git a/lib/modules/sideb/album_song_list/album_song_list_controller.dart b/lib/modules/sideb/album_song_list/album_song_list_controller.dart new file mode 100644 index 0000000..e70cc98 --- /dev/null +++ b/lib/modules/sideb/album_song_list/album_song_list_controller.dart @@ -0,0 +1,229 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/api/music_api.dart'; +import 'package:tone_snap/data/models/browse_model.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/collect_playlists_box.dart'; +import 'package:tone_snap/modules/sideb/collect_playlists/collect_playlists_controller.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/num_util.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class AlbumSongListController extends GetxController { + var musicPlayerController = MusicPlayerController.to; + var browseId = ''; + var params = ''; + + /// 封面 url、标题 、描述 + var coverUrl = ''.obs; + var title = ''.obs; + var subtitle = ''; + var description = ''.obs; + + var viewState = ViewState.loading.obs; + /// 专辑/歌单预览列表 + var musicList = >[].obs; + + /// 是否收藏 + var isCollect = false.obs; + + @override + void onInit() { + super.onInit(); + Map arguments = Get.arguments; + browseId = arguments['browseId'] ?? ''; + params = arguments['params'] ?? ''; + coverUrl.value = arguments['coverUrl'] ?? ''; + title.value = arguments['title'] ?? ''; + subtitle = arguments['subtitle'] ?? ''; + } + + @override + void onReady() async { + super.onReady(); + _checkIsCollect(); + await _requestBrowse(); + viewState.value = musicList.isNotEmpty ? ViewState.normal : ViewState.empty; + } + + void _checkIsCollect() { + var model = CollectPlaylistsBox().getList().firstWhereOrNull((e) => e.id == browseId); + isCollect.value = model != null; + } + + Future _requestBrowse() async { + Map queryParameters = { + 'browseId': browseId, + 'params': params, + 'prettyPrint': false + }; + BrowseModel? browseModel = await MusicApi.browse( + queryParameters: queryParameters, + formJson: BrowseModel.fromMap, + showToast: true, + ); + if (browseModel != null) { + var thumbnails = browseModel.background?.musicThumbnailRenderer?.thumbnail?.thumbnails; + if (thumbnails != null && thumbnails.isNotEmpty) { + coverUrl.value = thumbnails.last.url ?? ''; + } + var tabs = browseModel.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 = browseModel.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer?.contents; + if (contents != null && contents.isNotEmpty) { + List? contents1; + if (contents[0].musicShelfRenderer != null) { + contents1 = contents[0].musicShelfRenderer?.contents; + } else if (contents[0].musicPlaylistShelfRenderer != null) { + contents1 = contents[0].musicPlaylistShelfRenderer?.contents; + } + if (contents1 != null && contents1.isNotEmpty) { + for (var o in contents1) { + var musicModel = MusicModel(); + var thumbnails = o.musicResponsiveListItemRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails; + if (thumbnails != null && thumbnails.isNotEmpty) { + musicModel.coverUrl = thumbnails.last.url; + } + + var flexColumns = o.musicResponsiveListItemRenderer?.flexColumns; + if (flexColumns != null && flexColumns.isNotEmpty) { + var runs = flexColumns[0].musicResponsiveListItemFlexColumnRenderer?.text?.runs; + if (runs != null && runs.isNotEmpty) { + musicModel.title = runs[0].text; + if (runs[0].navigationEndpoint?.watchEndpoint != null) { + musicModel.videoId = runs[0].navigationEndpoint?.watchEndpoint?.videoId; + musicModel.playlistId = runs[0].navigationEndpoint?.watchEndpoint?.playlistId; + } + } + for (var o in flexColumns) { + var runs = o.musicResponsiveListItemFlexColumnRenderer?.text?.runs; + if (runs != null && runs.isNotEmpty) { + musicModel.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) { + musicModel.subtitle = '${ObjUtil.getStr(musicModel.subtitle)} • ${runs.map((e) => e.text).join()}'; + } + } + var playlistItemData = o.musicResponsiveListItemRenderer?.playlistItemData; + if (playlistItemData != null) { + musicModel.videoId = playlistItemData.videoId; + } + musicList.add(musicModel.obs); + } + } + } + } + } + + /// 点击播放全部歌曲 + Future onTapPlayAll() async { + if (musicList.isNotEmpty) { + final randomNumber = NumUtil.getRandomNumber(0, musicList.length); + List playList = await _next(musicList[randomNumber].value.videoId, playlistId: musicList[randomNumber].value.playlistId); + musicPlayerController.playMusic(playList[randomNumber].videoId, playList: playList); + } + } + + /// 获取播放列表 + Future> _next(String? videoId, {String? playlistId}) async { + List playList = []; + NextModel? model = await MusicApi.next(playlistId: playlistId, videoId: videoId, showLoading: true); + if (model != null) { + var tabs = model.contents?.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer?.tabs; + if (tabs != null && tabs.isNotEmpty) { + for (var i = 0; i < tabs.length; ++i) { + var o = tabs[i]; + if (i == 0) { + var contents = o.tabRenderer?.content?.musicQueueRenderer?.content?.playlistPanelRenderer?.contents; + if (contents != null && contents.isNotEmpty) { + for (var j = 0; j < contents.length; ++j) { + var musicModel = MusicModel(); + var content = contents[j]; + if (content.playlistPanelVideoRenderer != null) { + // 封面 + var thumbnails = content.playlistPanelVideoRenderer?.thumbnail?.thumbnails; + if (thumbnails != null) { + musicModel.coverUrl = thumbnails.last.url; + } + + // 标题 + var runs = content.playlistPanelVideoRenderer?.title?.runs; + if (runs != null && runs.isNotEmpty) { + musicModel.title = runs[0].text; + } + + // 副标题 + var subRuns = content.playlistPanelVideoRenderer?.longBylineText?.runs; + if (subRuns != null && subRuns.isNotEmpty) { + musicModel.subtitle = subRuns.map((e) => e.text).join(); + } + + // videoId, playlistId + var watchEndpoint = content.playlistPanelVideoRenderer?.navigationEndpoint?.watchEndpoint; + if (watchEndpoint != null) { + musicModel.videoId = watchEndpoint.videoId; + musicModel.playlistId = watchEndpoint.playlistId; + } + playList.add(musicModel); + } + } + } + } + } + } + } + return playList; + } + + /// 点击收藏 + Future onTapCollect() async { + if (ObjUtil.isNotEmpty(title.value) && musicList.isNotEmpty) { + if (isCollect.value) { + await CollectPlaylistsBox().delete(browseId); + BaseEasyLoading.toast('Removed'); + } else { + await CollectPlaylistsBox().add( + id: browseId, + title: title.value, + params: params, + coverUrl: coverUrl.value, + subtitle: subtitle, + ); + BaseEasyLoading.toast('Collected'); + } + _checkIsCollect(); + if (Get.isRegistered()) { + CollectPlaylistsController.to.getList(); + } + } + } + + /// 点击列表 + void onTapItem(MusicModel musicModel) { + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': musicModel.videoId, + 'playlistId': musicModel.playlistId, + }); + } +} diff --git a/lib/modules/sideb/album_song_list/album_song_list_view.dart b/lib/modules/sideb/album_song_list/album_song_list_view.dart new file mode 100644 index 0000000..c944326 --- /dev/null +++ b/lib/modules/sideb/album_song_list/album_song_list_view.dart @@ -0,0 +1,201 @@ +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/network_image_widget.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/sideb/album_song_list/album_song_list_controller.dart'; +import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart'; +import 'package:tone_snap/modules/sideb/widgets/music_item.dart'; +import 'package:tone_snap/modules/sideb/widgets/remove_padding_music_bar.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class AlbumSongListView extends StatelessWidget { + AlbumSongListView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF121212), + body: Stack( + children: [ + _buildPageBgImg(), + Column( + children: [ + SizedBox( + height: 413.h, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const MusicAppbar(), + _buildIntroduction(), + ], + ), + ), + _buildList(), + const RemovePaddingMusicBar(), + ], + ), + ], + ), + ); + } + + /// 封面图 + Widget _buildPageBgImg() { + return Obx(() { + return NetworkImageWidget( + url: controller.coverUrl.value, + width: 1.sw, + height: 413.h, + ); + }); + } + + Widget _buildIntroduction() { + return SizedBox( + height: 173.h, + child: 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.sideBPlaylistTitleBg, + width: 1.sw, + height: double.infinity, + fit: BoxFit.cover, + ), + ), + ), + ), + 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: 10.h), + Expanded( + child: Obx(() { + return Text( + ObjUtil.getStr(controller.description.value), + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0x99FFFFFF), + fontSize: 12.sp, + ), + ); + }), + ), + SizedBox(height: 10.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: controller.onTapPlayAll, + child: 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.sideBPlaylistPlayAllRandom, + width: 24.w, + height: 24.w, + ), + SizedBox(width: 4.w), + Expanded( + child: Obx(() { + return Text( + 'Play all (${controller.musicList.length})', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ); + }), + ), + ], + ), + ), + ), + ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: controller.onTapCollect, + child: Padding( + padding: const EdgeInsets.all(6).w, + child: Obx(() { + return Image.asset( + controller.isCollect.value ? Assets.sideBCollected : Assets.sideBNotCollectionAlbum, + width: 24.w, + height: 24.w, + ); + }), + ), + ), + ), + ), + ], + ), + SizedBox(height: 18.h), + ], + ), + ), + ], + ), + ); + } + + Widget _buildList() { + return Expanded( + child: Obx(() { + return ViewStateWidget( + viewState: controller.viewState.value, + child: BaseScrollbar( + child: Obx(() { + return ListView.builder( + itemCount: controller.musicList.length, + itemBuilder: (context, index) { + return MusicItem( + musicModel: controller.musicList[index], + onTapItem: () => controller.onTapItem(controller.musicList[index].value), + ); + }, + ); + }), + ), + ); + }), + ); + } +} diff --git a/lib/modules/sideb/artists/artists_binding.dart b/lib/modules/sideb/artists/artists_binding.dart new file mode 100644 index 0000000..45be0cb --- /dev/null +++ b/lib/modules/sideb/artists/artists_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'artists_controller.dart'; + +class ArtistsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => ArtistsController()); + } +} diff --git a/lib/modules/sideb/artists/artists_controller.dart b/lib/modules/sideb/artists/artists_controller.dart new file mode 100644 index 0000000..bf9384b --- /dev/null +++ b/lib/modules/sideb/artists/artists_controller.dart @@ -0,0 +1,22 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/models/music_model.dart'; + +class ArtistsController extends GetxController { + static ArtistsController get to => Get.find(); + var viewState = ViewState.loading.obs; + var artistsList = [].obs; + + @override + void onReady() { + super.onReady(); + viewState.value = artistsList.isNotEmpty ? ViewState.normal : ViewState.empty; + } + + void onTapItem(MusicModel model) { + + } + + void refreshList() { + } +} diff --git a/lib/modules/sideb/artists/artists_view.dart b/lib/modules/sideb/artists/artists_view.dart new file mode 100644 index 0000000..0fd7ad4 --- /dev/null +++ b/lib/modules/sideb/artists/artists_view.dart @@ -0,0 +1,123 @@ +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/my_marquee_text.dart'; +import 'package:tone_snap/components/network_image_widget.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart'; +import 'package:tone_snap/modules/sideb/widgets/remove_padding_music_bar.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +import 'artists_controller.dart'; + +class ArtistsView extends StatelessWidget { + ArtistsView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Stack( + children: [ + _buildPageBg(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MusicAppbar( + title: 'Love Artists', + ), + _buildTotal(), + _buildList(), + const RemovePaddingMusicBar(), + ], + ), + ], + ), + ); + } + + Widget _buildPageBg() { + return Image.asset( + Assets.sideBLoveSongsBg, + width: 1.sw, + height: 1.sh, + fit: BoxFit.cover, + ); + } + + Widget _buildTotal() { + return Padding( + padding: EdgeInsets.only(top: 34.h, bottom: 10.h, left: 18.w, right: 18.w), + child: Obx(() { + return Text( + '${controller.artistsList.length} Artists', + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + overflow: TextOverflow.ellipsis, + ), + ); + }), + ); + } + + Widget _buildList() { + return Expanded( + child: Obx(() { + return ViewStateWidget( + viewState: controller.viewState.value, + child: BaseScrollbar( + child: Obx(() { + return ListView.builder( + itemCount: controller.artistsList.length, + itemBuilder: (context, index) { + return _buildItem(controller.artistsList[index]); + }, + ); + }), + ), + ); + }), + ); + } + + Widget _buildItem(MusicModel model) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => controller.onTapItem(model), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 18.w), + child: Row( + children: [ + ClipOval( + child: NetworkImageWidget( + url: model.coverUrl, + width: 50.w, + height: 50.w, + ), + ), + SizedBox(width: 14.w), + Expanded( + child: Obx(() { + return MyMarqueeText( + text: ObjUtil.getStr(model.title), + textStyle: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ); + }), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/sideb/collect_playlists/collect_playlists_binding.dart b/lib/modules/sideb/collect_playlists/collect_playlists_binding.dart deleted file mode 100644 index a0ccb9d..0000000 --- a/lib/modules/sideb/collect_playlists/collect_playlists_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -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 index 0c7026f..8d0b1d6 100644 --- a/lib/modules/sideb/collect_playlists/collect_playlists_controller.dart +++ b/lib/modules/sideb/collect_playlists/collect_playlists_controller.dart @@ -1,12 +1,29 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/models/playlist_model.dart'; +import 'package:tone_snap/data/storage/collect_playlists_box.dart'; class CollectPlaylistsController extends GetxController { - ScrollController scrollController = ScrollController(); + static CollectPlaylistsController get to => Get.find(); + var viewState = ViewState.loading.obs; + var scrollController = ScrollController(); + var playLists = [].obs; + + @override + void onReady() { + super.onReady(); + getList(); + } @override void onClose() { scrollController.dispose(); super.onClose(); } + + void getList() { + playLists.value = CollectPlaylistsBox().getReversedList(); + viewState.value = playLists.isNotEmpty ? ViewState.normal : ViewState.empty; + } } diff --git a/lib/modules/sideb/collect_playlists/collect_playlists_view.dart b/lib/modules/sideb/collect_playlists/collect_playlists_view.dart index 5b96848..79b9e6d 100644 --- a/lib/modules/sideb/collect_playlists/collect_playlists_view.dart +++ b/lib/modules/sideb/collect_playlists/collect_playlists_view.dart @@ -1,8 +1,9 @@ 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/components/view_state_widget.dart'; +import 'package:tone_snap/modules/sideb/widgets/playlist_item.dart'; +import 'package:tone_snap/modules/sideb/widgets/remove_padding_music_bar.dart'; import 'collect_playlists_controller.dart'; @@ -11,63 +12,34 @@ class CollectPlaylistsView extends GetView { @override Widget build(BuildContext context) { - Get.find(); + Get.put(CollectPlaylistsController()); - 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, - ), - ), - ], + return Column( + children: [ + Expanded( + child: Obx(() { + return ViewStateWidget( + viewState: controller.viewState.value, + child: BaseScrollbar( + scrollController: controller.scrollController, + child: Obx(() { + return ListView.builder( + controller: controller.scrollController, + itemCount: controller.playLists.length, + itemBuilder: (context, index) { + return PlaylistItem( + index: 1, + playlistModel: controller.playLists[index], + ); + }, + ); + }), ), - ), - ], + ); + }), ), - ), + const RemovePaddingMusicBar(), + ], ); } } diff --git a/lib/modules/sideb/controllers/main_controller.dart b/lib/modules/sideb/controllers/main_controller.dart new file mode 100644 index 0000000..27528c2 --- /dev/null +++ b/lib/modules/sideb/controllers/main_controller.dart @@ -0,0 +1,23 @@ +// Author: fengshengxiong +// Date: 2024/8/1 +// Description: 主程序入口控制器 + +import 'package:get/get.dart'; +import 'package:tone_snap/data/storage/music_box.dart'; + +class MainController extends GetxController { + static MainController get to => Get.find(); + var isSideBTheme = false.obs; + + @override + void onInit() { + super.onInit(); + bool isOpenedSideB = MusicBox().getIsOpenedSideB(); + isSideBTheme.value = isOpenedSideB; + } + + void changeTheme() { + isSideBTheme.value = true; + update(['changeTheme']); + } +} \ No newline at end of file diff --git a/lib/modules/sideb/controllers/music_player_controller.dart b/lib/modules/sideb/controllers/music_player_controller.dart index fd64113..f9e0b0a 100644 --- a/lib/modules/sideb/controllers/music_player_controller.dart +++ b/lib/modules/sideb/controllers/music_player_controller.dart @@ -8,17 +8,19 @@ 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/data/storage/offline_box.dart'; +import 'package:tone_snap/modules/sideb/music_bar/music_bar.dart'; import 'package:tone_snap/routes/app_routes.dart'; import 'package:tone_snap/utils/audio_util.dart'; import 'package:tone_snap/utils/log_util.dart'; import 'package:tone_snap/utils/num_util.dart'; +import 'package:tone_snap/utils/obj_util.dart'; class MusicPlayerController extends GetxController { static MusicPlayerController get to => Get.put(MusicPlayerController(), permanent: true); @@ -46,27 +48,24 @@ class MusicPlayerController extends GetxController { /// _player.seek(Duration.zero) 时,这可能会导致播放状态再次变为 completed。 /// 为了避免这种情况,在处理 completed 状态时添加一个标志位,确保在处理完成状态时不会重复触发 /// 是否已经处理过 completed 状态 - bool _isCompletedHandled = false; + var _isCompletedHandled = false; /// 拖动开始时是否是播放状态 - var seekFrontPlaying = true; + var _seekFrontPlaying = true; /// 播放模式,默认列表循环 var playMode = MusicBox().getPlayMode().obs; var _playModeIndex = 0; - /// 播放列表 - var playList = [].obs; - /// 播放历史 videoId 集合 /// 随机播放切换到上一首时,从历史记录中取出上一个播放的 videoId final List _playHistory = []; - /// 播放的歌曲模型 - var musicModel = MusicModel().obs; + /// 播放列表 + var playlist = >[].obs; /// 当前播放的歌曲索引 - int _currentIndex = 0; + var currentIndex = 0.obs; @override void onInit() { @@ -82,86 +81,74 @@ class MusicPlayerController extends GetxController { super.onClose(); } - /// 播放指定索引的歌曲 - void playMusicSpecifyIndex(int index) { - _currentIndex = index; - playMusic(); + void playMusic(String? videoId, {List? playList, bool isCurrentPlaylist = false}) { + if (videoId == null) return; + if (!isCurrentPlaylist) { + if (playList == null || playList.isEmpty) return; + _playHistory.clear(); + playlist.value = playList.map((e) => e.obs).toList(); + } + Rx? model = playlist.firstWhereOrNull((e) => e.value.videoId == videoId); + currentIndex.value = model != null ? playlist.indexOf(model) : 0; + _startPlay(); } - /// 设置播放列表 - 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; + Rx? getMusicModel() { + return playlist.isNotEmpty ? playlist[currentIndex.value] : null; } /// 播放歌曲 - 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(); + Future _startPlay() async { + if (playlist.isNotEmpty) { + resetPlaybackStatus(); + Get.currentRoute == AppRoutes.playPage ? MusicBar().hide() : MusicBar().show(); + try { + MusicModel? model = OfflineBox().getList().firstWhereOrNull((e) => e.videoId == getMusicModel()!.value.videoId!); + if (model != null) { + // 有下载 + LogUtil.d('读取下载路径=${model.localPath}'); + await _player.setFilePath(model.localPath!); + } else { + // 无下载 + FileInfo? fileInfo = await MusicCacheManager.checkCache(getMusicModel()!.value.videoId!); + if (fileInfo != null) { + // 有缓存 + LogUtil.d('读取缓存路径=${fileInfo.file.path}'); + // 如果有缓存,使用缓存文件 + await _player.setFilePath(fileInfo.file.path); + } else { + // 无缓存 + final url = await _getMusicUrl(); + if (ObjUtil.isEmpty(url)) { + return; + } + await _player.setUrl(url!); + // 同时启动缓存下载 + _cacheManager.downloadFile(url, key: MusicCacheManager.getCacheKey(getMusicModel()!.value.videoId!)).then((fileInfo) { + LogUtil.d('缓存下载路径=${fileInfo.file.path}'); + }); + } + } + _cancelListening(); + _addListening(); + _player.play(); + } catch (e) { + LogUtil.e('Play failed: $e'); + BaseEasyLoading.toast('Play failed'); } - 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); + Future _getMusicUrl() async { + PlayerModel? model = await MusicApi.player(videoId: playlist[currentIndex.value].value.videoId); if (model != null && model.streamingData != null) { var formats = model.streamingData?.formats; - if (formats != null && playList.isNotEmpty) { - playList[_currentIndex].url = formats[0].url; + if (formats != null && playlist.isNotEmpty) { + return formats[0].url; } } - if (model != null && model.videoDetails?.thumbnail?.thumbnails != null) { - - } - } - - /// 设置 url 和监听器 - Future _initializePlayer() async { - try { - // 检查是否有缓存 - FileInfo? fileInfo = await MusicCacheManager.checkCache(musicModel.value.videoId!); - if (fileInfo != null) { - LogUtil.d('读取缓存=${fileInfo.file.path}'); - - // 如果有缓存,使用缓存文件 - await _player.setFilePath(fileInfo.file.path); - } else { - // 如果没有缓存,通过 url 加载音源并开始播放, - await _player.setUrl(musicModel.value.url!); - - // 同时启动缓存下载 - _cacheManager.downloadFile(musicModel.value.url!, key: MusicCacheManager.getCacheKey(musicModel.value.videoId!)).then((fileInfo) { - LogUtil.d('缓存下载成功=${fileInfo.file.path}'); - }); - } - _cancelListening(); - _addListening(); - _player.play(); - } catch (e) { - LogUtil.e('Error initializing player: $e'); - BaseEasyLoading.toast('Error initializing player'); - } + return null; } /// 监听 @@ -171,7 +158,12 @@ class MusicPlayerController extends GetxController { }); _positionSubscription = _player.positionStream.listen((position) { - positionDuration.value = position; + int comparison = position.compareTo(totalDuration.value); + if (comparison > 0) { + positionDuration.value = totalDuration.value; + } else { + positionDuration.value = position; + } }); _bufferedSubscription = _player.bufferedPositionStream.listen((bufferedPosition) { @@ -227,13 +219,13 @@ class MusicPlayerController extends GetxController { Future seekStartEnd(int value) async { if (processingState.value == ProcessingState.ready) { if (value == 0) { - seekFrontPlaying = _player.playing; + _seekFrontPlaying = _player.playing; if (_player.playing) { _player.pause(); } } else { // 拖动前如果是播放状态,拖动完成后恢复播放 - if (seekFrontPlaying) { + if (_seekFrontPlaying) { _player.play(); } } @@ -252,20 +244,20 @@ class MusicPlayerController extends GetxController { } /// 上一首 - void previousTrack() { - if (playList.isNotEmpty) { + Future previousTrack() async { + if (playlist.length > 1) { switch(playMode.value) { case PlayMode.listLoop: - _currentIndex = (_currentIndex - 1 + playList.length) % playList.length; - playMusic(); + currentIndex.value = (currentIndex.value - 1 + playlist.length) % playlist.length; + _startPlay(); 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); + Rx? model = playlist.firstWhereOrNull((e) => e.value.videoId == history); if (model != null) { - _currentIndex = playList.indexOf(model); + currentIndex.value = playlist.indexOf(model); _playHistory.remove(history); historyExist = true; break; @@ -274,9 +266,9 @@ class MusicPlayerController extends GetxController { } } if (!historyExist) { - _getRandomNumber(); + currentIndex.value = _getRandomNumber(); } - playMusic(); + _startPlay(); break; case PlayMode.singleCycle: break; @@ -285,18 +277,18 @@ class MusicPlayerController extends GetxController { } /// 下一首 - void nextTrack() { - if (playList.isNotEmpty) { + Future nextTrack() async { + if (playlist.length > 1) { switch(playMode.value) { case PlayMode.listLoop: - _currentIndex = (_currentIndex + 1) % playList.length; - playMusic(); + currentIndex.value = (currentIndex.value + 1) % playlist.length; + _startPlay(); break; case PlayMode.random: // 记录当前播放的索引 - _playHistory.add(musicModel.value.videoId!); - _getRandomNumber(); - playMusic(); + _playHistory.add(getMusicModel()!.value.videoId!); + currentIndex.value = _getRandomNumber(); + _startPlay(); break; case PlayMode.singleCycle: break; @@ -305,28 +297,36 @@ class MusicPlayerController extends GetxController { } /// 在列表范围内生成一个不包括当前索引的随机数 - void _getRandomNumber() { - _currentIndex = NumUtil.getRandomNumberExcludingCurrent(0, playList.length, _currentIndex); + int _getRandomNumber() { + return NumUtil.getRandomNumberExcludingCurrent(0, playlist.length, currentIndex.value); } /// 切换播放模式 void switchPlayMode() { - if (_playModeIndex == PlayMode.values.length - 1) { + if (_playModeIndex >= PlayMode.values.length - 1) { _playModeIndex = 0; } else { _playModeIndex++; } playMode.value = PlayMode.values[_playModeIndex]; MusicBox().putPlayMode(playMode.value); + if (playMode.value == PlayMode.listLoop) { + BaseEasyLoading.toast('List loop'); + } + if (playMode.value == PlayMode.random) { + BaseEasyLoading.toast('Shuffle Playback'); + } + if (playMode.value == PlayMode.singleCycle) { + BaseEasyLoading.toast('Single cycle'); + } } /// 重置播放状态 - void _resetPlaybackStatus() { - _player.stop(); + Future resetPlaybackStatus() async { + await _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/custom_playlist/custom_playlist_binding.dart b/lib/modules/sideb/custom_playlist/custom_playlist_binding.dart new file mode 100644 index 0000000..ba06829 --- /dev/null +++ b/lib/modules/sideb/custom_playlist/custom_playlist_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'custom_playlist_controller.dart'; + +class CustomPlaylistBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => CustomPlaylistController()); + } +} diff --git a/lib/modules/sideb/custom_playlist/custom_playlist_controller.dart b/lib/modules/sideb/custom_playlist/custom_playlist_controller.dart new file mode 100644 index 0000000..ca2e54f --- /dev/null +++ b/lib/modules/sideb/custom_playlist/custom_playlist_controller.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/models/playlist_model.dart'; +import 'package:tone_snap/data/storage/playlists_box.dart'; +import 'package:tone_snap/modules/sideb/initial/initial_controller.dart'; +import 'package:tone_snap/modules/sideb/more_playlist_bottom_sheet/more_playlist_bottom_sheet_view.dart'; +import 'package:tone_snap/routes/app_routes.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class CustomPlaylistController extends GetxController { + static CustomPlaylistController get to => Get.find(); + late String playlistModelId; + var playlistModel = Rx(null); + var musicList = >[].obs; + var showSearch = false.obs; + var textEditingController = TextEditingController(); + + @override + void onInit() { + super.onInit(); + playlistModelId = Get.arguments; + getPlaylistMode(); + } + + @override + void onClose() { + textEditingController.dispose(); + super.onClose(); + } + + void getPlaylistMode() { + playlistModel.value = PlaylistsBox().getPlaylistModel(playlistModelId); + playlistModel.update((fn){}); + _getList(); + } + + void _getList() { + musicList.value = playlistModel.value!.musicList!.map((e) => e.obs).toList(); + } + + String? getFirstCoverUrl() { + if (musicList.isNotEmpty) { + return musicList.first.value.coverUrl; + } + return null; + } + + void onTapMore() { + Get.bottomSheet( + MorePlaylistBottomSheetView( + playlistModel: playlistModel.value!, + ), + ); + } + + void onTapShowSearch(bool show) { + if (show && musicList.isEmpty) { + return; + } + showSearch.value = show; + if (!showSearch.value) { + textEditingController.clear(); + _getList(); + } + } + + void onTapSearch(String keywords) { + if (ObjUtil.isNotEmpty(keywords)) { + final result = >[].obs; + playlistModel.value!.musicList!.map((e) { + if (e.title!.toLowerCase().contains(keywords.trim().toLowerCase())) { + result.add(e.obs); + } + }).toList(); + musicList.value = result; + } else { + _getList(); + } + } + + void onTapItem(MusicModel musicModel) { + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': musicModel.videoId, + 'playList': musicList.map((e) => e.value).toList(), + }); + } + + void onTapAddSongs() { + Get.until((route) => route.isFirst); + InitialController.to.onBottomAppBarItemChanged(0); + } +} diff --git a/lib/modules/sideb/custom_playlist/custom_playlist_view.dart b/lib/modules/sideb/custom_playlist/custom_playlist_view.dart new file mode 100644 index 0000000..5700572 --- /dev/null +++ b/lib/modules/sideb/custom_playlist/custom_playlist_view.dart @@ -0,0 +1,290 @@ +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/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/modules/sideb/widgets/music_item.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'; + +import 'custom_playlist_controller.dart'; + +class CustomPlaylistView extends StatelessWidget { + CustomPlaylistView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + _buildPlaylistInfo(), + _buildPlayAllSearch(), + _buildList(), + ], + ), + ); + } + + Widget _buildPlaylistInfo() { + return SizedBox( + height: 271.h, + child: Stack( + children: [ + _buildCoverBg(), + MusicAppbar( + actionOnTap: controller.onTapMore, + action: Image.asset( + Assets.sideBMore, + width: 24.w, + height: 24.w, + color: Colors.white, + ), + ), + Positioned( + top: 80.h, + left: 0, + right: 0, + child: Column( + children: [ + _buildCover(), + _buildTitle(), + _buildSubtitle(), + ], + ), + ), + ], + ), + ); + } + + Widget _buildCoverBg() { + return ClipRect( + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 12.w, sigmaY: 12.w), + child: Obx(() { + return NetworkImageWidget( + url: controller.getFirstCoverUrl(), + width: double.infinity, + height: double.infinity, + ); + }), + ), + ); + } + + Widget _buildCover() { + return Obx(() { + return NetworkImageWidget( + url: controller.getFirstCoverUrl(), + width: 120.w, + height: 120.w, + placeholderWidth: 60.w, + placeholderHeight: 60.w, + radius: 8.r, + ); + }); + } + + Widget _buildTitle() { + return Padding( + padding: const EdgeInsets.only(top: 20.0).h, + child: Obx(() { + return MyMarqueeText( + text: ObjUtil.getStr(controller.playlistModel.value!.title), + textStyle: TextStyle( + color: Colors.white, + fontSize: 18.sp, + ), + ); + }), + ); + } + + Widget _buildSubtitle() { + return Padding( + padding: const EdgeInsets.only(top: 2).h, + child: Text( + 'Created in: ${controller.playlistModel.value!.milliseconds != null ? DateUtil.formatDateMs(controller.playlistModel.value!.milliseconds!) : ''}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + ), + ), + ); + } + + Widget _buildPlayAllSearch() { + return SizedBox( + height: 50.h, + child: Obx(() { + return Visibility( + visible: controller.showSearch.value, + replacement: Row( + children: [ + SizedBox(width: 18.w), + Expanded( + child: Row( + children: [ + Image.asset( + Assets.sideBPlaylistPlayAll, + width: 24.w, + height: 24.w, + ), + SizedBox(width: 6.w), + Expanded( + child: Obx(() { + return Text( + 'Play all (${controller.musicList.length})', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ); + }), + ), + ], + ), + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => controller.onTapShowSearch(true), + child: Container( + height: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 18).w, + child: Image.asset( + Assets.sideBSearch, + width: 24.w, + height: 24.w, + ), + ), + ), + ], + ), + child: Row( + children: [ + SizedBox(width: 18.w), + Expanded( + child: Container( + height: 25.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0x1AFFFFFF), + borderRadius: BorderRadius.circular(12).r, + ), + child: TextField( + maxLines: 1, + controller: controller.textEditingController, + textInputAction: TextInputAction.done, + textAlign: TextAlign.start, + keyboardType: TextInputType.text, + style: TextStyle(color: seedColor, fontSize: 12.sp), + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + counterText: '', + hintText: 'name', + hintStyle: TextStyle(color: const Color(0xFF999999), fontSize: 12.sp,), + contentPadding: const EdgeInsets.symmetric(horizontal: 12).w, + isCollapsed: true, + border: InputBorder.none, + ), + onChanged: (text) { + controller.onTapSearch(text); + }, + ), + ), + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => controller.onTapShowSearch(false), + child: Container( + height: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 18).w, + alignment: Alignment.center, + child: Text( + 'Cancel', + style: TextStyle( + color: seedColor, + fontSize: 14.sp, + ), + ), + ), + ), + ], + ), + ); + }), + ); + } + + Widget _buildList() { + return Expanded( + child: BaseScrollbar( + child: Obx(() { + return Visibility( + visible: controller.musicList.isNotEmpty, + replacement: _buildEmpty(), + child: ListView.builder( + itemCount: controller.musicList.length, + itemBuilder: (context, index) { + return MusicItem( + musicModel: controller.musicList[index], + playlistModelId: controller.playlistModel.value!.id, + onTapItem: () => controller.onTapItem(controller.musicList[index].value), + ); + }, + ), + ); + }), + ), + ); + } + + Widget _buildEmpty() { + return Column( + children: [ + SizedBox(height: 40.h), + Text( + 'Nothing Yet', + style: TextStyle( + color: const Color(0x99FFFFFF), + fontSize: 14.sp, + ), + ), + SizedBox(height: 16.h), + GestureDetector( + onTap: controller.onTapAddSongs, + child: Container( + width: 122.w, + height: 35.h, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(40).r, + border: Border.all( + color: seedColor, + ), + ), + child: Text( + 'Add Songs', + style: TextStyle( + color: seedColor, + fontSize: 16.sp, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/modules/sideb/home/home_binding.dart b/lib/modules/sideb/home/home_binding.dart deleted file mode 100644 index 0905989..0000000 --- a/lib/modules/sideb/home/home_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; - -import 'home_controller.dart'; - -class HomeBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => HomeController()); - } -} diff --git a/lib/modules/sideb/home/home_controller.dart b/lib/modules/sideb/home/home_controller.dart index aaf39f0..ca9a304 100644 --- a/lib/modules/sideb/home/home_controller.dart +++ b/lib/modules/sideb/home/home_controller.dart @@ -1,25 +1,26 @@ import 'package:get/get.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/enum/music_type.dart'; +import 'package:tone_snap/data/models/browse_group_model.dart'; import 'package:tone_snap/data/models/browse_model.dart'; -import 'package:tone_snap/data/models/home_model.dart'; +import 'package:tone_snap/data/models/music_model.dart'; import 'package:tone_snap/routes/app_routes.dart'; -import 'package:tone_snap/utils/obj_util.dart'; class HomeController extends GetxController { + static HomeController get to => Get.find(); var viewState = ViewState.loading.obs; - var homeList = [].obs; + var groupList = [].obs; String? visitorData; @override void onReady() { super.onReady(); - _firstBrowse(); + firstBrowse(); } /// 首次请求 - Future _firstBrowse() async { + Future firstBrowse() async { Map queryParameters = { 'prettyPrint': false, 'browseId': 'FEmusic_home' @@ -41,6 +42,7 @@ class HomeController extends GetxController { } _reBrowse(continuation: continuation, clickTrackingParams: clickTrackingParams); } + viewState.value = groupList.isNotEmpty ? ViewState.normal : ViewState.empty; } /// 复用请求 @@ -76,18 +78,18 @@ class HomeController extends GetxController { } if (contents != null && contents.isNotEmpty) { for (var e1 in contents) { - HomeModel model = HomeModel(); + var browseGroupModel = BrowseGroupModel(); // 获取头部标题 var runs = e1.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs; if (runs != null && runs.isNotEmpty) { - model.headerTitle = runs[0].text; + browseGroupModel.groupTitle = runs[0].text; } var contents = e1.musicCarouselShelfRenderer?.contents; if (contents != null) { - model.contents = []; + browseGroupModel.browseList ??= []; for (var e2 in contents) { - Content content = Content(); + var musicModel = MusicModel(); // 获取封面缩略图 List? thumbnails; @@ -101,23 +103,23 @@ class HomeController extends GetxController { if (flexColumns.indexOf(e3) == 0) { if (runs != null && runs.isNotEmpty) { // 获取主标题 - content.title = runs[0].text; + musicModel.title = runs[0].text; // 获取数据类型 if (runs[0].navigationEndpoint?.watchEndpoint != null) { - model.browseType = runs[0].navigationEndpoint?.watchEndpoint?.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig?.musicVideoType; - content.videoId = runs[0].navigationEndpoint?.watchEndpoint?.videoId; - content.playlistId = runs[0].navigationEndpoint?.watchEndpoint?.playlistId; + browseGroupModel.musicType = runs[0].navigationEndpoint?.watchEndpoint?.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig?.musicVideoType; + musicModel.videoId = runs[0].navigationEndpoint?.watchEndpoint?.videoId; + musicModel.playlistId = runs[0].navigationEndpoint?.watchEndpoint?.playlistId; } 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; + browseGroupModel.musicType = runs[0].navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType; + musicModel.browseId = runs[0].navigationEndpoint?.browseEndpoint?.browseId; + musicModel.params = runs[0].navigationEndpoint?.browseEndpoint?.params; } } } else { // 获取副标题 if (runs != null && runs.isNotEmpty) { - content.subTitle = (ObjUtil.isNotEmptyStr(content.subTitle) ? '${content.subTitle} • ' : '') + runs.map((e) => e.text).join(); + musicModel.subtitle = runs.map((e) => e.text).join(' • '); } } } @@ -128,44 +130,58 @@ class HomeController extends GetxController { // 获取主标题 var runs = e2.musicTwoRowItemRenderer?.title?.runs; if (runs != null && runs.isNotEmpty) { - content.title = runs[0].text; + musicModel.title = runs[0].text; } // 获取副标题 var subtitleRuns = e2.musicTwoRowItemRenderer?.subtitle?.runs; if (subtitleRuns != null && subtitleRuns.isNotEmpty) { - content.subTitle = subtitleRuns.map((e) => e.text).join(); + musicModel.subtitle = subtitleRuns.map((e) => e.text).join(); } // 获取数据类型 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; + browseGroupModel.musicType = e2.musicTwoRowItemRenderer?.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType; + musicModel.browseId = e2.musicTwoRowItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId; + musicModel.params = e2.musicTwoRowItemRenderer?.navigationEndpoint?.browseEndpoint?.params; } } if (thumbnails != null && thumbnails.isNotEmpty) { - content.thumbnail = thumbnails.last.url; + musicModel.coverUrl = thumbnails.last.url; } - model.contents?.add(content); + browseGroupModel.browseList?.add(musicModel); } } // 根据类型判断是否添加 - if (BrowseTypeExtension.isThereAny(model.browseType)) { - homeList.add(model); + if (MusicTypeExtension.isThereAny(browseGroupModel.musicType)) { + groupList.add(browseGroupModel); } } } - viewState.value = homeList.isNotEmpty ? ViewState.normal : ViewState.empty; - } - - /// 打开播放页面 - 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}); + void openSearch() { + Get.toNamed(AppRoutes.searchResult); + } + + void openSetting() { + Get.toNamed(AppRoutes.setting); + } + + void openPlayPage(MusicModel musicModel) { + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': musicModel.videoId, + 'playlistId': musicModel.playlistId, + }); + } + + void openAlbumSong(MusicModel musicModel) { + Get.toNamed(AppRoutes.albumSongList, arguments: { + 'browseId': musicModel.browseId, + 'params': musicModel.params, + 'coverUrl': musicModel.coverUrl, + 'title': musicModel.title, + 'subtitle': musicModel.subtitle, + }); } } diff --git a/lib/modules/sideb/home/home_view.dart b/lib/modules/sideb/home/home_view.dart index cc44a7c..bdcf4a5 100644 --- a/lib/modules/sideb/home/home_view.dart +++ b/lib/modules/sideb/home/home_view.dart @@ -3,13 +3,12 @@ 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/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/enum/music_type.dart'; +import 'package:tone_snap/data/models/browse_group_model.dart'; import 'package:tone_snap/generated/assets.dart'; -import 'package:tone_snap/modules/sideb/widgets/omv_item.dart'; +import 'package:tone_snap/modules/sideb/widgets/browse_item_album_song_list.dart'; +import 'package:tone_snap/modules/sideb/widgets/browse_item_atv.dart'; +import 'package:tone_snap/modules/sideb/widgets/remove_padding_music_bar.dart'; import 'package:tone_snap/utils/obj_util.dart'; import 'home_controller.dart'; @@ -19,108 +18,169 @@ class HomeView extends GetView { @override Widget build(BuildContext context) { - Get.find(); - return Column( - children: [ - _buildTitle(), - _buildListView(context), - ], - ); - } + Get.put(HomeController()); - Widget _buildTitle() { - return MusicAppbar( - title: 'Musicoo', - showBackWidget: false, - actionOnTap: () {}, - action: Image.asset( - Assets.sideBLineMenu, - width: 24.w, - height: 24.w, + return SafeArea( + child: Column( + children: [ + _buildTitle(), + _buildList(context), + const RemovePaddingMusicBar(), + ], ), ); } - Widget _buildListView(BuildContext context) { + Widget _buildTitle() { + return Container( + margin: const EdgeInsets.symmetric(vertical: 10).h, + height: 32.h, + child: Row( + children: [ + SizedBox(width: 18.w), + Expanded( + child: GestureDetector( + onTap: controller.openSearch, + child: Container( + height: 32.h, + padding: EdgeInsets.symmetric(horizontal: 18.w), + decoration: BoxDecoration( + color: const Color(0xFF212121), + borderRadius: BorderRadius.circular(20).r, + ), + child: Row( + children: [ + Image.asset( + Assets.sideBSearchWhite, + width: 16.w, + height: 16.w, + ), + SizedBox(width: 10.w), + Text( + 'Search songs,playlists', + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0xFF666666), + fontSize: 14.sp, + ), + ), + ], + ), + ), + ), + ), + SizedBox(width: 12.w), + ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: controller.openSetting, + child: Padding( + padding: const EdgeInsets.all(6).w, + child: Image.asset( + Assets.sideBLineMenu, + width: 24.w, + height: 24.w, + ), + ), + ), + ), + ), + SizedBox(width: 12.w), + ], + ), + ); + } + + Widget _buildList(BuildContext context) { return Expanded( 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); - }, - ), + child: Obx(() { + return ListView.separated( + padding: const EdgeInsets.only(bottom: 16).h, + itemCount: controller.groupList.length, + separatorBuilder: (context, index) => SizedBox(height: 16.h), + itemBuilder: (context, index) { + final browseGroupModel = controller.groupList[index]; + if (!MusicTypeExtension.isThereAny(browseGroupModel.musicType)) { + return Container(); + } + return Column( + children: [ + _buildGroupTitle(ObjUtil.getStr(browseGroupModel.groupTitle)), + if (browseGroupModel.musicType == MusicType.musicVideoTypeAtv.name) ...[ + _buildGroupAtv(browseGroupModel), + ] else ...[ + _buildGroupPlaylist(browseGroupModel), + ], + ], + ); + }, + ); + }), ), ); }), ); } - 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; - } + Widget _buildGroupAtv(BrowseGroupModel browseGroupModel) { 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); - }, - ), - ), - ], + height: 200.h, + child: GridView.builder( + padding: EdgeInsets.symmetric(horizontal: 16.w), + scrollDirection: Axis.horizontal, + itemCount: browseGroupModel.browseList != null ? browseGroupModel.browseList!.length : 0, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 0, + crossAxisSpacing: 10.h, + childAspectRatio: 60.w / (1.sw - 16.w), + ), + itemBuilder: (context, index) { + final musicModel = browseGroupModel.browseList![index]; + return BrowseItemAtv( + musicModel: musicModel, + onTap: () => controller.openPlayPage(musicModel), + ); + }, ), ); } - Widget _labelWidget(String label) { + Widget _buildGroupPlaylist(BrowseGroupModel browseGroupModel) { + return SizedBox( + height: 160.h, + child: ListView.separated( + padding: EdgeInsets.symmetric(horizontal: 16.w), + scrollDirection: Axis.horizontal, + itemCount: browseGroupModel.browseList != null ? browseGroupModel.browseList!.length : 0, + separatorBuilder: (context, index) => SizedBox(width: 8.w), + itemBuilder: (context, index) { + final musicModel = browseGroupModel.browseList![index]; + return BrowseItemAlbumSongList( + musicModel: musicModel, + onTap: () => controller.openAlbumSong(musicModel), + ); + }, + ), + ); + } + + Widget _buildGroupTitle(String title, {bool showMore = false}) { return InkWell( onTap: () {}, - child: SizedBox( + child: Container( height: 40.h, + padding: const EdgeInsets.symmetric(horizontal: 16).w, child: Row( children: [ Expanded( child: Text( - label, + title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -129,12 +189,14 @@ class HomeView extends GetView { ), ), ), - SizedBox(width: 16.w), - Image.asset( - Assets.sideBArrowRightItem, - width: 24.w, - height: 24.w, - ), + if (showMore) ...[ + SizedBox(width: 16.w), + Image.asset( + Assets.sideBArrowRightItem, + width: 24.w, + height: 24.w, + ), + ], ], ), ), diff --git a/lib/modules/sideb/initial/initial_controller.dart b/lib/modules/sideb/initial/initial_controller.dart index e63849c..77b81e3 100644 --- a/lib/modules/sideb/initial/initial_controller.dart +++ b/lib/modules/sideb/initial/initial_controller.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/global/app_tracking_transparency_manager.dart'; import 'package:tone_snap/modules/sideb/home/home_view.dart'; +import 'package:tone_snap/modules/sideb/personal_music_library/personal_music_library_controller.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'; @@ -18,6 +20,7 @@ class InitialController extends GetxController { @override void onInit() { super.onInit(); + AppTrackingTransparencyManager().requestATT(); pageController = PageController(initialPage: currentIndex.value); } @@ -30,6 +33,13 @@ class InitialController extends GetxController { Future onBottomAppBarItemChanged(int index) async { currentIndex.value = index; pageController.jumpToPage(index); + + if (index == 2) { + if (Get.isRegistered()) { + PersonalMusicLibraryController.to.refreshLoveSongs(); + PersonalMusicLibraryController.to.refreshOffline(); + } + } } } diff --git a/lib/modules/sideb/initial/initial_view.dart b/lib/modules/sideb/initial/initial_view.dart index deab69c..f399835 100644 --- a/lib/modules/sideb/initial/initial_view.dart +++ b/lib/modules/sideb/initial/initial_view.dart @@ -26,11 +26,17 @@ class InitialView extends StatelessWidget { } Widget _buildPageBg() { - return Image.asset( - Assets.sideBHomeBg, - width: 1.sw, - fit: BoxFit.fitWidth, - ); + return Obx(() { + return Visibility( + visible: controller.currentIndex.value != 2, + child: Image.asset( + Assets.sideBHomeBg, + width: 1.sw, + height: 1.sh, + fit: BoxFit.cover, + ), + ); + }); } Widget _buildBody() { @@ -49,7 +55,7 @@ class InitialView extends StatelessWidget { borderRadius: BorderRadius.only(topLeft: Radius.circular(24.r), topRight: Radius.circular(24.r)), image: const DecorationImage( image: AssetImage(Assets.sideBBnbBg), - fit: BoxFit.fill, + fit: BoxFit.cover, ), ), child: Row( diff --git a/lib/modules/sideb/love_songs/love_songs_controller.dart b/lib/modules/sideb/love_songs/love_songs_controller.dart index cabb5a1..1634bf1 100644 --- a/lib/modules/sideb/love_songs/love_songs_controller.dart +++ b/lib/modules/sideb/love_songs/love_songs_controller.dart @@ -1,13 +1,29 @@ import 'package:get/get.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; import 'package:tone_snap/data/models/music_model.dart'; import 'package:tone_snap/data/storage/love_songs_box.dart'; +import 'package:tone_snap/routes/app_routes.dart'; class LoveSongsController extends GetxController { - var loveList = []; + static LoveSongsController get to => Get.find(); + var viewState = ViewState.loading.obs; + var loveList = >[].obs; @override - void onInit() { - super.onInit(); - loveList = LoveSongsBox().getList(); + void onReady() { + super.onReady(); + getLoveList(); + } + + void getLoveList() { + loveList.value = LoveSongsBox().getReversedList().map((e) => e.obs).toList(); + viewState.value = loveList.isNotEmpty ? ViewState.normal : ViewState.empty; + } + + void onTapItem(MusicModel musicModel) { + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': musicModel.videoId, + 'playList': LoveSongsBox().getReversedList(), + }); } } diff --git a/lib/modules/sideb/love_songs/love_songs_view.dart b/lib/modules/sideb/love_songs/love_songs_view.dart index 98c2d6b..5094316 100644 --- a/lib/modules/sideb/love_songs/love_songs_view.dart +++ b/lib/modules/sideb/love_songs/love_songs_view.dart @@ -2,9 +2,11 @@ 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/components/view_state_widget.dart'; +import 'package:tone_snap/generated/assets.dart'; import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart'; +import 'package:tone_snap/modules/sideb/widgets/music_item.dart'; +import 'package:tone_snap/modules/sideb/widgets/remove_padding_music_bar.dart'; import 'love_songs_controller.dart'; @@ -16,92 +18,71 @@ class LoveSongsView extends StatelessWidget { @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(), - ], - ), + backgroundColor: Colors.transparent, + body: Stack( + children: [ + _buildPageBg(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MusicAppbar( + title: 'Love Songs', + ), + _buildTotal(), + _buildList(), + const RemovePaddingMusicBar(), + ], + ), + ], ), ); } + Widget _buildPageBg() { + return Image.asset( + Assets.sideBLoveSongsBg, + width: 1.sw, + height: 1.sh, + fit: BoxFit.cover, + ); + } + 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, - ), - ), + child: Obx(() { + return Text( + '${controller.loveList.length} Songs', + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + overflow: TextOverflow.ellipsis, + ), + ); + }), ); } - Widget _buildListView() { + Widget _buildList() { 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, - ), - ), - ], - ), - ), - ], - ), - ), + child: Obx(() { + return ViewStateWidget( + viewState: controller.viewState.value, + child: BaseScrollbar( + child: Obx(() { + return ListView.builder( + itemCount: controller.loveList.length, + itemBuilder: (context, index) { + return MusicItem( + musicModel: controller.loveList[index], + onTapItem: () => controller.onTapItem(controller.loveList[index].value), + ); + }, + ); + }), + ), + ); + }), ); } } diff --git a/lib/modules/sideb/more_bottom_sheet/more_bottom_sheet_controller.dart b/lib/modules/sideb/more_bottom_sheet/more_bottom_sheet_controller.dart new file mode 100644 index 0000000..c63fa32 --- /dev/null +++ b/lib/modules/sideb/more_bottom_sheet/more_bottom_sheet_controller.dart @@ -0,0 +1,107 @@ +import 'package:background_downloader/background_downloader.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/components/dialog/remind_dialog.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/storage/love_songs_box.dart'; +import 'package:tone_snap/data/storage/offline_box.dart'; +import 'package:tone_snap/data/storage/playlists_box.dart'; +import 'package:tone_snap/global/download_manager.dart'; +import 'package:tone_snap/modules/sideb/add_to_playlist_bottom_sheet/add_to_playlist_bottom_sheet_view.dart'; +import 'package:tone_snap/modules/sideb/custom_playlist/custom_playlist_controller.dart'; +import 'package:tone_snap/modules/sideb/love_songs/love_songs_controller.dart'; +import 'package:tone_snap/modules/sideb/offline/offline_controller.dart'; +import 'package:tone_snap/modules/sideb/personal_music_library/personal_music_library_controller.dart'; +import 'package:tone_snap/modules/sideb/playlists/playlists_controller.dart'; + +class MoreBottomSheetController extends GetxController { + late Rx musicModel; + String? playlistModelId; + var isLove = false.obs; + + void setMusicModel(Rx musicModel, {String? playlistModelId}) { + this.musicModel = musicModel; + this.playlistModelId = playlistModelId; + _checkIsLove(); + } + + void _checkIsLove() { + isLove.value = LoveSongsBox().checkLove(musicModel.value.videoId); + } + + void onTapLove() async { + if (musicModel.value.videoId == null) return; + if (isLove.value) { + await LoveSongsBox().delete(musicModel.value.videoId!); + BaseEasyLoading.toast('Removed'); + } else { + await LoveSongsBox().add(musicModel.value.copyWith()); + BaseEasyLoading.toast('Collected'); + } + _checkIsLove(); + if (Get.isRegistered()) { + PersonalMusicLibraryController.to.refreshLoveSongs(); + } + if (Get.isRegistered()) { + LoveSongsController.to.getLoveList(); + } + } + + void onTapDownload() { + if (OfflineBox().checkDownloaded(musicModel.value.videoId) || musicModel.value.taskStatus == TaskStatus.complete) { + Get.dialog( + RemindDialog( + content: 'Confirm to remove this song?', + confirmOnTap: () async { + await OfflineBox().delete(musicModel.value.videoId!); + musicModel.update((fn) => fn?.taskStatus = null); + BaseEasyLoading.toast('Removed'); + if (Get.isRegistered()) { + PersonalMusicLibraryController.to.refreshOffline(); + } + if (Get.isRegistered()) { + OfflineController.to.getOfflineList(); + } + }, + ), + ); + } else { + if (musicModel.value.taskStatus == TaskStatus.enqueued + || musicModel.value.taskStatus == TaskStatus.running) { + DownloadManager().cancelDownload(musicModel); + } else { + DownloadManager().downloadMusic(musicModel); + } + } + } + + void onTapAddToPlaylist() { + Get.back(); + Get.bottomSheet( + AddToPlaylistBottomSheetView( + musicModel: musicModel.value, + ), + ); + } + + Future onTapRemove() async { + if (playlistModelId != null && musicModel.value.videoId != null) { + bool result = await PlaylistsBox().removeMusic(playlistModelId!, musicModel.value.videoId!); + if (result) { + BaseEasyLoading.toast('Removed'); + Get.back(); + if (Get.isRegistered()) { + PlaylistsController.to.getList(); + } + if (Get.isRegistered()) { + CustomPlaylistController.to.getPlaylistMode(); + } + } + } + } + + void onTapReport() { + Get.back(); + BaseEasyLoading.toast('We have received your feedback and will deal with it as soon as possible'); + } +} diff --git a/lib/modules/sideb/more_bottom_sheet/more_bottom_sheet_view.dart b/lib/modules/sideb/more_bottom_sheet/more_bottom_sheet_view.dart new file mode 100644 index 0000000..10f9a4a --- /dev/null +++ b/lib/modules/sideb/more_bottom_sheet/more_bottom_sheet_view.dart @@ -0,0 +1,237 @@ +import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/divider_widget.dart'; +import 'package:tone_snap/components/get_bind_widget.dart'; +import 'package:tone_snap/components/network_image_widget.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/storage/offline_box.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +import 'more_bottom_sheet_controller.dart'; + +class MoreBottomSheetView extends StatelessWidget { + MoreBottomSheetView({super.key, required this.musicModel, this.playlistModelId}); + + final controller = Get.put(MoreBottomSheetController()); + final Rx musicModel; + final String? playlistModelId; + + @override + Widget build(BuildContext context) { + controller.setMusicModel(musicModel, playlistModelId: playlistModelId); + return GetBindWidget( + bind: controller, + child: IntrinsicHeight( + child: Container( + width: 1.sw, + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), + decoration: const BoxDecoration( + color: Color(0xFF282A2C), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(18), topRight: Radius.circular(18), + ), + ), + child: Column( + children: [ + _buildIndicator(), + _buildMusicInfo(), + _buildDownload(), + _buildItem('Add to playlist', Assets.sideBAddToPlaylist, controller.onTapAddToPlaylist), + // _buildItem('Add to queue', Assets.sideBAddToQueue, () {}), + if (playlistModelId != null) ...[ + _buildItem('Remove from list', Assets.sideBMoreRemove, controller.onTapRemove), + ], + _buildItem('Report', Assets.sideBReport, controller.onTapReport), + ], + ), + ), + ), + ); + } + + Widget _buildIndicator() { + return Container( + width: 28, + height: 4, + margin: const EdgeInsets.symmetric(vertical: 8).h, + decoration: BoxDecoration( + color: const Color(0xFF555555), + borderRadius: BorderRadius.circular(20), + ), + ); + } + + Widget _buildMusicInfo() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 18.w), + child: Column( + children: [ + SizedBox(height: 20.h), + Row( + children: [ + Obx(() { + return NetworkImageWidget( + url: musicModel.value.coverUrl, + width: 50.w, + height: 50.w, + radius: 10.r, + ); + }), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ObjUtil.getStr(musicModel.value.title), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + Text( + ObjUtil.getStr(musicModel.value.subtitle), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0xFF666666), + fontSize: 12.sp, + ), + ), + ], + ), + ), + GestureDetector( + onTap: controller.onTapLove, + child: ClipOval( + child: Container( + width: 32.w, + height: 32.w, + color: const Color(0x26FFFFFF), + child: FittedBox( + fit: BoxFit.none, + child: Obx(() { + return Image.asset( + controller.isLove.value ? Assets.sideBLoveSolid : Assets.sideBLove, + width: 16.w, + height: 16.w, + ); + }), + ), + ), + ), + ), + ], + ), + SizedBox(height: 15.5.h), + const DividerWidget(color: Color(0xFF444444)), + ], + ), + ); + } + + Widget _buildDownload() { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: controller.onTapDownload, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 12.h), + child: Row( + children: [ + SizedBox( + width: 24.w, + height: 24.w, + child: Obx(() { + if (OfflineBox().checkDownloaded(musicModel.value.videoId) || musicModel.value.taskStatus == TaskStatus.complete) { + return Image.asset(Assets.sideBDownloaded); + } else { + if (musicModel.value.taskStatus == TaskStatus.enqueued) { + return CircularProgressIndicator( + color: seedColor, + strokeWidth: 2.w, + ); + } else if (musicModel.value.taskStatus == TaskStatus.running) { + return CircularProgressIndicator( + value: musicModel.value.progress, + backgroundColor: seedColor.withOpacity(0.2), + valueColor: const AlwaysStoppedAnimation(seedColor), + strokeWidth: 2.w, + ); + } else { + return Image.asset(Assets.sideBNotDownload1); + } + } + }), + ), + SizedBox(width: 12.w), + Expanded( + child: Obx(() { + var text = ''; + if (OfflineBox().checkDownloaded(musicModel.value.videoId) || musicModel.value.taskStatus == TaskStatus.complete) { + text = ' Removed'; + } else { + if (musicModel.value.taskStatus == TaskStatus.enqueued + || musicModel.value.taskStatus == TaskStatus.running) { + text = 'Cancel download'; + } else { + text = 'Download'; + } + } + return Text( + text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ); + }), + ), + ], + ), + ), + ), + ); + } + + Widget _buildItem(String label, String img, Function() onTap) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 12.h), + child: Row( + children: [ + Image.asset( + img, + width: 24.w, + height: 24.w, + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/sideb/more_playlist_bottom_sheet/more_playlist_bottom_sheet_controller.dart b/lib/modules/sideb/more_playlist_bottom_sheet/more_playlist_bottom_sheet_controller.dart new file mode 100644 index 0000000..412c65f --- /dev/null +++ b/lib/modules/sideb/more_playlist_bottom_sheet/more_playlist_bottom_sheet_controller.dart @@ -0,0 +1,52 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/components/dialog/add_playlist_dialog.dart'; +import 'package:tone_snap/components/dialog/remind_dialog.dart'; +import 'package:tone_snap/data/models/playlist_model.dart'; +import 'package:tone_snap/data/storage/playlists_box.dart'; +import 'package:tone_snap/modules/sideb/custom_playlist/custom_playlist_controller.dart'; +import 'package:tone_snap/modules/sideb/playlists/playlists_controller.dart'; + +class MorePlaylistBottomSheetController extends GetxController { + late PlaylistModel playlistModel; + + void setPlaylistModel(PlaylistModel playlistModel) { + this.playlistModel = playlistModel; + } + + void onTapEdit() { + Get.back(); + Get.dialog( + barrierDismissible: false, + CreatePlaylistDialog( + name: playlistModel.title, + onTap: (value) async { + PlaylistsBox().editTitle(playlistModel.id, value); + if (Get.isRegistered()) { + CustomPlaylistController.to.getPlaylistMode(); + } + if (Get.isRegistered()) { + PlaylistsController.to.getList(); + } + }, + ), + ); + } + + void onTapRemove() { + Get.back(); + Get.dialog( + RemindDialog( + content: 'Confirm to delete this playlist?', + confirmOnTap: () async { + await PlaylistsBox().delete(playlistModel.id); + BaseEasyLoading.toast('Removed'); + Get.until((route) => route.isFirst); + if (Get.isRegistered()) { + PlaylistsController.to.getList(); + } + }, + ), + ); + } +} diff --git a/lib/modules/sideb/more_playlist_bottom_sheet/more_playlist_bottom_sheet_view.dart b/lib/modules/sideb/more_playlist_bottom_sheet/more_playlist_bottom_sheet_view.dart new file mode 100644 index 0000000..7c16600 --- /dev/null +++ b/lib/modules/sideb/more_playlist_bottom_sheet/more_playlist_bottom_sheet_view.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/get_bind_widget.dart'; +import 'package:tone_snap/data/models/playlist_model.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/sideb/more_playlist_bottom_sheet/more_playlist_bottom_sheet_controller.dart'; + +class MorePlaylistBottomSheetView extends StatelessWidget { + MorePlaylistBottomSheetView({super.key, required this.playlistModel}); + + final controller = Get.put(MorePlaylistBottomSheetController()); + final PlaylistModel playlistModel; + + @override + Widget build(BuildContext context) { + controller.setPlaylistModel(playlistModel); + return GetBindWidget( + bind: controller, + child: IntrinsicHeight( + child: Container( + width: 1.sw, + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), + decoration: const BoxDecoration( + color: Color(0xFF282A2C), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(18), topRight: Radius.circular(18), + ), + ), + child: Column( + children: [ + _buildIndicator(), + _buildTitle(), + _buildItem('Edit', Assets.sideBMoreEdit, controller.onTapEdit), + _buildItem('Delete', Assets.sideBMoreRemove, controller.onTapRemove), + ], + ), + ), + ), + ); + } + + Widget _buildTitle() { + return Padding( + padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 18.w), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + playlistModel.title, + textAlign: TextAlign.start, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + ), + ); + } + + Widget _buildIndicator() { + return Container( + width: 28, + height: 4, + margin: const EdgeInsets.symmetric(vertical: 8).h, + decoration: BoxDecoration( + color: const Color(0xFF555555), + borderRadius: BorderRadius.circular(20), + ), + ); + } + + Widget _buildItem(String label, String img, Function() onTap) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 12.h), + child: Row( + children: [ + Image.asset( + img, + width: 24.w, + height: 24.w, + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/components/music_bar.dart b/lib/modules/sideb/music_bar/music_bar.dart similarity index 86% rename from lib/components/music_bar.dart rename to lib/modules/sideb/music_bar/music_bar.dart index d70ba64..d4d40a1 100644 --- a/lib/components/music_bar.dart +++ b/lib/modules/sideb/music_bar/music_bar.dart @@ -4,7 +4,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:tone_snap/components/music_bar/music_bar_view.dart'; + +import 'music_bar_view.dart'; class MusicBar { static final MusicBar _instance = MusicBar._(); @@ -16,6 +17,9 @@ class MusicBar { /// 显示音乐栏 show() { + if (Get.isBottomSheetOpen != null && Get.isBottomSheetOpen!) { + return; + } if (overlayEntry == null && Get.context != null) { overlayEntry = OverlayEntry(builder: (BuildContext context) { return MusicBarView(); diff --git a/lib/components/music_bar/music_bar_binding.dart b/lib/modules/sideb/music_bar/music_bar_binding.dart similarity index 100% rename from lib/components/music_bar/music_bar_binding.dart rename to lib/modules/sideb/music_bar/music_bar_binding.dart diff --git a/lib/modules/sideb/music_bar/music_bar_controller.dart b/lib/modules/sideb/music_bar/music_bar_controller.dart new file mode 100644 index 0000000..517062b --- /dev/null +++ b/lib/modules/sideb/music_bar/music_bar_controller.dart @@ -0,0 +1,35 @@ +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(); + var bottom = 0.0.obs; + + @override + void onInit() { + super.onInit(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (Get.currentRoute == AppRoutes.initialB) { + bottom.value = kBottomNavigationBarHeight; + } + }); + } + + /// 底部导航栏消失时沉底 + void toBottom() { + bottom.value = 0.0; + } + + /// 底部导航栏出现时抬高 + void riseUp() { + bottom.value = kBottomNavigationBarHeight; + } + + /// 打开播放页面 + void openPlayPage() { + Get.toNamed(AppRoutes.playPage, arguments: { + 'isFormMusicBar': true, + }); + } +} diff --git a/lib/modules/sideb/music_bar/music_bar_view.dart b/lib/modules/sideb/music_bar/music_bar_view.dart new file mode 100644 index 0000000..752da1b --- /dev/null +++ b/lib/modules/sideb/music_bar/music_bar_view.dart @@ -0,0 +1,133 @@ +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/generated/assets.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 SafeArea( + child: Stack( + fit: StackFit.expand, + children: [ + Obx(() { + return AnimatedPositioned( + bottom: controller.bottom.value, + left: 16.w, + right: 16.w, + duration: const Duration(milliseconds: 200), + child: GestureDetector( + onTap: controller.openPlayPage, + child: Container( + width: 1.sw - 32.w, + height: musicBarHeight, + padding: EdgeInsets.symmetric(horizontal: 22.w), + decoration: BoxDecoration( + color: const Color(0xFF80F988), + borderRadius: BorderRadius.circular(36).r, + ), + child: Row( + children: [ + Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 52.w, + height: 52.w, + child: Obx(() { + int comparison = musicPlayerController.positionDuration.value.compareTo(musicPlayerController.totalDuration.value); + double value = comparison < 0 ? (musicPlayerController.positionDuration.value.inSeconds / musicPlayerController.totalDuration.value.inSeconds).toDouble() : 0; + return CircularProgressIndicator( + value: value, + backgroundColor: Colors.transparent, + strokeWidth: 4.w, + valueColor: const AlwaysStoppedAnimation(Colors.white), + ); + }), + ), + Obx(() { + return ClipOval( + child: FittedBox( + fit: BoxFit.none, + child: NetworkImageWidget( + url: musicPlayerController.getMusicModel()?.value.coverUrl, + 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.getMusicModel()?.value.title), + textStyle: TextStyle( + color: Colors.black, + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ); + }), + SizedBox(height: 4.h), + Obx(() { + return MyMarqueeText( + text: ObjUtil.getStr(musicPlayerController.getMusicModel()?.value.subtitle), + textStyle: TextStyle( + color: Colors.black, + fontSize: 12.sp, + ), + ); + }), + ], + ), + ), + const SizedBox(width: 13), + Obx(() { + return GestureDetector( + onTap: musicPlayerController.playPause, + child: Image.asset( + musicPlayerController.isPlaying.value ? Assets.sideBPausePlay : Assets.sideBStartPlay, + width: 24.w, + height: 24.w, + ), + ); + }), + const SizedBox(width: 26), + GestureDetector( + onTap: musicPlayerController.nextTrack, + child: Image.asset( + Assets.sideBMusicBarNext, + width: 24.w, + height: 24.w, + ), + ), + ], + ), + ), + ), + ); + }), + ], + ), + ); + } +} diff --git a/lib/modules/sideb/offline/offline_binding.dart b/lib/modules/sideb/offline/offline_binding.dart new file mode 100644 index 0000000..289ffe6 --- /dev/null +++ b/lib/modules/sideb/offline/offline_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'offline_controller.dart'; + +class OfflineBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => OfflineController()); + } +} diff --git a/lib/modules/sideb/offline/offline_controller.dart b/lib/modules/sideb/offline/offline_controller.dart new file mode 100644 index 0000000..9d0b135 --- /dev/null +++ b/lib/modules/sideb/offline/offline_controller.dart @@ -0,0 +1,29 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/storage/offline_box.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class OfflineController extends GetxController { + static OfflineController get to => Get.find(); + var viewState = ViewState.loading.obs; + var offlineList = >[].obs; + + @override + void onReady() { + super.onReady(); + getOfflineList(); + } + + void getOfflineList() { + offlineList.value = OfflineBox().getReversedList().map((e) => e.obs).toList(); + viewState.value = offlineList.isNotEmpty ? ViewState.normal : ViewState.empty; + } + + void onTapItem(MusicModel model) { + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': model.videoId, + 'playList': OfflineBox().getReversedList(), + }); + } +} diff --git a/lib/modules/sideb/offline/offline_view.dart b/lib/modules/sideb/offline/offline_view.dart new file mode 100644 index 0000000..e790b57 --- /dev/null +++ b/lib/modules/sideb/offline/offline_view.dart @@ -0,0 +1,89 @@ +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/view_state_widget.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart'; +import 'package:tone_snap/modules/sideb/widgets/music_item.dart'; +import 'package:tone_snap/modules/sideb/widgets/remove_padding_music_bar.dart'; + +import 'offline_controller.dart'; + +class OfflineView extends StatelessWidget { + OfflineView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Stack( + children: [ + _buildPageBg(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MusicAppbar( + title: 'Offline Songs', + ), + _buildTotal(), + _buildList(), + const RemovePaddingMusicBar(), + ], + ), + ], + ), + ); + } + + Widget _buildPageBg() { + return Image.asset( + Assets.sideBLoveSongsBg, + width: 1.sw, + height: 1.sh, + fit: BoxFit.cover, + ); + } + + Widget _buildTotal() { + return Padding( + padding: EdgeInsets.only(top: 34.h, bottom: 10.h, left: 18.w, right: 18.w), + child: Obx(() { + return Text( + '${controller.offlineList.length} Songs', + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + overflow: TextOverflow.ellipsis, + ), + ); + }), + ); + } + + Widget _buildList() { + return Expanded( + child: Obx(() { + return ViewStateWidget( + viewState: controller.viewState.value, + child: BaseScrollbar( + child: Obx(() { + return ListView.builder( + itemCount: controller.offlineList.length, + itemBuilder: (context, index) { + return MusicItem( + musicModel: controller.offlineList[index], + showDownload: false, + onTapItem: () => controller.onTapItem(controller.offlineList[index].value), + ); + }, + ); + }), + ), + ); + }), + ); + } +} 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 deleted file mode 100644 index c51b1e8..0000000 --- a/lib/modules/sideb/personal_music_library/personal_music_library_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -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 index 211b3d2..fe9c389 100644 --- a/lib/modules/sideb/personal_music_library/personal_music_library_controller.dart +++ b/lib/modules/sideb/personal_music_library/personal_music_library_controller.dart @@ -1,10 +1,22 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tone_snap/data/storage/love_songs_box.dart'; +import 'package:tone_snap/data/storage/offline_box.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 { + static PersonalMusicLibraryController get to => Get.find(); + var loveSongsCoverUrl = ''.obs; + var loveSongsTotal = 0.obs; + + var artistsCoverUrl = ''.obs; + var artistsTotal = 0.obs; + + var offlineCoverUrl = ''.obs; + var offlineTotal = 0.obs; + late TabController tabController; final labels = ['Playlists', 'Collect Playlists']; final pages = [const PlaylistsView(), const CollectPlaylistsView()]; @@ -13,6 +25,8 @@ class PersonalMusicLibraryController extends GetxController with GetSingleTicker void onInit() { super.onInit(); tabController = TabController(length: labels.length, vsync: this); + refreshLoveSongs(); + refreshOffline(); } @override @@ -26,10 +40,20 @@ class PersonalMusicLibraryController extends GetxController with GetSingleTicker Get.toNamed(AppRoutes.loveSongs); } if (index == 1) { - Get.toNamed(AppRoutes.loveSongs); + Get.toNamed(AppRoutes.artists); } if (index == 2) { - Get.toNamed(AppRoutes.loveSongs); + Get.toNamed(AppRoutes.offline); } } + + void refreshLoveSongs() { + loveSongsCoverUrl.value = LoveSongsBox().getFirstCoverUrl() ?? ''; + loveSongsTotal.value = LoveSongsBox().getList().length; + } + + void refreshOffline() { + offlineCoverUrl.value = OfflineBox().getFirstCoverUrl() ?? ''; + offlineTotal.value = OfflineBox().getList().length; + } } 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 index a2479db..4663608 100644 --- a/lib/modules/sideb/personal_music_library/personal_music_library_view.dart +++ b/lib/modules/sideb/personal_music_library/personal_music_library_view.dart @@ -6,6 +6,7 @@ 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 'package:tone_snap/utils/obj_util.dart'; import 'personal_music_library_controller.dart'; @@ -14,7 +15,8 @@ class PersonalMusicLibraryView extends GetView { @override Widget build(BuildContext context) { - Get.find(); + Get.put(PersonalMusicLibraryController()); + return Stack( children: [ _buildPageBg(), @@ -37,7 +39,8 @@ class PersonalMusicLibraryView extends GetView { return Image.asset( Assets.sideBPersonalMusicLibraryBg, width: 1.sw, - fit: BoxFit.fitWidth, + height: 1.sh, + fit: BoxFit.cover, ); } @@ -58,35 +61,52 @@ class PersonalMusicLibraryView extends GetView { return Padding( padding: EdgeInsets.symmetric(vertical: 28.h, horizontal: 16.w), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + // 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)), + _buildModuleItem(0, Assets.sideBLoveSolid, () => controller.onTapModule(0)), + SizedBox(width: 16.w), + // _buildModuleItem(1, Assets.sideBArtists, () => controller.onTapModule(1)), + _buildModuleItem(2, Assets.sideBOfflineDownload, () => controller.onTapModule(2)), ], ), ); } - Widget _buildModuleItem(String url, String labelImg, String label, Function() onTap) { + Widget _buildModuleItem(int index, String labelImg, Function() onTap) { return GestureDetector( onTap: onTap, child: Container( width: 109.w, height: 109.w, + clipBehavior: Clip.antiAlias, 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, - ), + child: Obx(() { + String? url; + if (index == 0) url = controller.loveSongsCoverUrl.value; + if (index == 1) url = controller.artistsCoverUrl.value; + if (index == 2) url = controller.offlineCoverUrl.value; + return Stack( + children: [ + NetworkImageWidget( + url: url, + width: 109.w, + height: 109.w, + radius: 8.r, + placeholderWidth: 55.w, + placeholderHeight: 55.w, + placeholderImg: Assets.sideBPlaceholderLibrary, + ), + Container( + color: const Color(0x66000000), + ), + ], + ); + }), ), Positioned( bottom: 8.h, @@ -101,16 +121,22 @@ class PersonalMusicLibraryView extends GetView { ), SizedBox(width: 4.w), Expanded( - child: Text( - label, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - height: 1, - ), - ), + child: Obx(() { + String? label; + if (index == 0) label = 'Songs ${controller.loveSongsTotal.value}'; + if (index == 1) label = 'Artists ${controller.artistsTotal.value}'; + if (index == 2) label = 'Offline ${controller.offlineTotal.value}'; + return Text( + ObjUtil.getStr(label), + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + height: 1, + ), + ); + }), ), ], ), diff --git a/lib/modules/sideb/play_page/play_page_controller.dart b/lib/modules/sideb/play_page/play_page_controller.dart index a351cc7..8baed35 100644 --- a/lib/modules/sideb/play_page/play_page_controller.dart +++ b/lib/modules/sideb/play_page/play_page_controller.dart @@ -1,50 +1,39 @@ +import 'package:background_downloader/background_downloader.dart'; import 'package:get/get.dart'; import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/components/dialog/remind_dialog.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/data/storage/offline_box.dart'; +import 'package:tone_snap/global/download_manager.dart'; import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart'; +import 'package:tone_snap/modules/sideb/personal_music_library/personal_music_library_controller.dart'; class PlayPageController extends GetxController { var musicPlayerController = MusicPlayerController.to; - /// 是否从 MusicBar 打开 - bool isMusicBarOpen = false; - - /// 是否添加到喜欢 - var isLove = false.obs; - - /// 当前播放的歌曲信息 - late String videoId; - String? playlistId; - List? playList; - - @override - void onInit() { - super.onInit(); - videoId = Get.arguments['videoId'] ?? ''; - playlistId = Get.arguments['playlistId']; - playList = Get.arguments['playList']; - isMusicBarOpen = Get.arguments['isMusicBarOpen'] ?? false; - - isLove.value = LoveSongsBox().isLove(videoId); - } - @override void onReady() async { super.onReady(); - if (!isMusicBarOpen) { - if (musicPlayerController.musicModel.value.videoId != videoId) { - playList ??= await _next(); - musicPlayerController.setPlayList(videoId, playList!); - musicPlayerController.playMusic(); + bool isFormMusicBar = Get.arguments['isFormMusicBar'] ?? false; + String videoId = Get.arguments['videoId'] ?? ''; + String playlistId = Get.arguments['playlistId'] ?? ''; + List playList = Get.arguments['playList'] ?? []; + + if (!isFormMusicBar) { + if (playList.isEmpty) { + musicPlayerController.playlist.clear(); + musicPlayerController.resetPlaybackStatus(); + playList = await _next(videoId, playlistId); } + musicPlayerController.playMusic(videoId, playList: playList); } } /// 获取播放列表 - Future> _next() async { + Future> _next(String? videoId, String? playlistId) async { List playList = []; NextModel? model = await MusicApi.next(playlistId: playlistId, videoId: videoId); if (model != null) { @@ -56,33 +45,35 @@ class PlayPageController 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 = MusicModel(); + var musicModel = MusicModel(); var content = contents[j]; - // 封面 - var thumbnails = content.playlistPanelVideoRenderer?.thumbnail?.thumbnails; - if (thumbnails != null) { - playListModel.thumbnail = thumbnails.last.url; - } + if (content.playlistPanelVideoRenderer != null) { + // 封面 + var thumbnails = content.playlistPanelVideoRenderer?.thumbnail?.thumbnails; + if (thumbnails != null) { + musicModel.coverUrl = thumbnails.last.url; + } - // 标题 - var runs = content.playlistPanelVideoRenderer?.title?.runs; - if (runs != null && runs.isNotEmpty) { - playListModel.title = runs[0].text; - } + // 标题 + var runs = content.playlistPanelVideoRenderer?.title?.runs; + if (runs != null && runs.isNotEmpty) { + musicModel.title = runs[0].text; + } - // 副标题 - var subRuns = content.playlistPanelVideoRenderer?.longBylineText?.runs; - if (subRuns != null && subRuns.isNotEmpty) { - playListModel.subTitle = subRuns.map((e) => e.text).join(); - } + // 副标题 + var subRuns = content.playlistPanelVideoRenderer?.longBylineText?.runs; + if (subRuns != null && subRuns.isNotEmpty) { + musicModel.subtitle = subRuns.map((e) => e.text).join(); + } - // videoId, playlistId - var watchEndpoint = content.playlistPanelVideoRenderer?.navigationEndpoint?.watchEndpoint; - if (watchEndpoint != null) { - playListModel.videoId = watchEndpoint.videoId; - playListModel.playlistId = watchEndpoint.playlistId; + // videoId, playlistId + var watchEndpoint = content.playlistPanelVideoRenderer?.navigationEndpoint?.watchEndpoint; + if (watchEndpoint != null) { + musicModel.videoId = watchEndpoint.videoId; + musicModel.playlistId = watchEndpoint.playlistId; + } + playList.add(musicModel); } - playList.add(playListModel); } } } @@ -92,29 +83,57 @@ class PlayPageController extends GetxController { return playList; } - /// 加入/取消喜欢 - Future addCancelLove() async { - if (isLove.value) { - await LoveSongsBox().delete(musicPlayerController.musicModel.value.videoId!); - isLove.value = false; - BaseEasyLoading.toast('Cancelled'); + /// 加入/取消收藏 + Future onTapLove() async { + if (musicPlayerController.getMusicModel()?.value.videoId == null) return; + final isLove = LoveSongsBox().checkLove(musicPlayerController.getMusicModel()!.value.videoId); + if (isLove) { + await LoveSongsBox().delete(musicPlayerController.getMusicModel()!.value.videoId!); + BaseEasyLoading.toast('Removed'); } else { - await LoveSongsBox().addData(musicPlayerController.musicModel.value.copyWith()); - isLove.value = true; + await LoveSongsBox().add(musicPlayerController.getMusicModel()!.value.copyWith()); BaseEasyLoading.toast('Collected'); } + musicPlayerController.getMusicModel()!.update((fn) => fn?.isLove = LoveSongsBox().checkLove(musicPlayerController.getMusicModel()!.value.videoId)); + } + + void onTapDownload() { + if (OfflineBox().checkDownloaded(musicPlayerController.getMusicModel()?.value.videoId) + || musicPlayerController.getMusicModel()?.value.taskStatus == TaskStatus.complete) { + Get.dialog( + RemindDialog( + content: 'Confirm to remove this song?', + confirmOnTap: () async { + await OfflineBox().delete(musicPlayerController.getMusicModel()!.value.videoId!); + musicPlayerController.getMusicModel()!.update((fn) => fn?.taskStatus = null); + BaseEasyLoading.toast('Removed'); + if (Get.isRegistered()) { + PersonalMusicLibraryController.to.refreshOffline(); + } + }, + ), + ); + } else { + if (musicPlayerController.getMusicModel()?.value.taskStatus == TaskStatus.enqueued + || musicPlayerController.getMusicModel()?.value.taskStatus == TaskStatus.running) { + DownloadManager().cancelDownload(musicPlayerController.getMusicModel()); + } else { + DownloadManager().downloadMusic(musicPlayerController.getMusicModel()); + } + } } /// 在播放列表中切换歌曲 - void switchMusic(int index, MusicModel model) { - if (musicPlayerController.musicModel.value.videoId != model.videoId) { - musicPlayerController.playMusicSpecifyIndex(index); - } + void switchMusic(MusicModel musicModel) { Get.back(); + musicPlayerController.playMusic(musicModel.videoId, isCurrentPlaylist: true); } /// 删除播放列表中的歌曲 - void deleteMusic(int index) { - musicPlayerController.playList.removeAt(index); + void deleteMusic(int index, MusicModel musicModel) { + musicPlayerController.playlist.removeAt(index); + if (index < musicPlayerController.currentIndex.value) { + musicPlayerController.currentIndex.value--; + } } -} +} \ No newline at end of file diff --git a/lib/modules/sideb/play_page/play_page_view.dart b/lib/modules/sideb/play_page/play_page_view.dart index 0f0dc95..b0e53f0 100644 --- a/lib/modules/sideb/play_page/play_page_view.dart +++ b/lib/modules/sideb/play_page/play_page_view.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; @@ -9,7 +10,8 @@ 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/data/storage/love_songs_box.dart'; +import 'package:tone_snap/data/storage/offline_box.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'; @@ -26,47 +28,44 @@ class PlayPageView extends StatelessWidget { @override Widget build(BuildContext context) { - return Stack( - children: [ - _buildPageBgImg(), - _buildPageBgColor(), - Scaffold( - backgroundColor: Colors.transparent, - body: Column( + return Scaffold( + body: Stack( + children: [ + _buildPageBgImg(), + 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), - ], - ), + Padding( + 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(), + const Spacer(), + _buildPlayControlButtons(context), SizedBox(height: 67.h), ], ), - ) - ], + ], + ), ); } /// 页面背景图 Widget _buildPageBgImg() { return Obx(() { - return Visibility( - visible: ObjUtil.isNotEmptyStr(musicPlayerController.musicModel.value.thumbnail), + return ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 50.0, sigmaY: 50.0), child: NetworkImageWidget( - url: musicPlayerController.musicModel.value.thumbnail, + url: musicPlayerController.getMusicModel()?.value.coverUrl, width: 1.sw, height: 1.sh, noPlaceholder: true, @@ -75,69 +74,59 @@ class PlayPageView extends StatelessWidget { }); } - /// 页面背景色 - 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( + return const 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, - ), + // 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 _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, + url: musicPlayerController.getMusicModel()?.value.coverUrl, width: 1.sw, height: 330.h, radius: 16.r, + placeholderWidth: 100.w, + placeholderHeight: 100.w, ); }); } @@ -152,7 +141,7 @@ class PlayPageView extends StatelessWidget { children: [ Obx(() { return MyMarqueeText( - text: ObjUtil.getStr(musicPlayerController.musicModel.value.title), + text: ObjUtil.getStr(musicPlayerController.getMusicModel()?.value.title), textStyle: TextStyle( color: const Color(0xD9FFFFFF), fontSize: 22.sp, @@ -162,7 +151,7 @@ class PlayPageView extends StatelessWidget { SizedBox(height: 6.h), Obx(() { return MyMarqueeText( - text: ObjUtil.getStr(musicPlayerController.musicModel.value.subTitle), + text: ObjUtil.getStr(musicPlayerController.getMusicModel()?.value.subtitle), textStyle: TextStyle( color: const Color(0x99EEEEEE), fontSize: 12.sp, @@ -173,41 +162,73 @@ class PlayPageView extends StatelessWidget { ), ), 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, + Obx(() { + return Visibility( + visible: musicPlayerController.getMusicModel() != null, + child: ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: controller.onTapLove, + child: Padding( + padding: const EdgeInsets.all(6).w, + child: Obx(() { + return Image.asset( + LoveSongsBox().checkLove(musicPlayerController.getMusicModel()?.value.videoId) ? Assets.sideBLoveSolid : Assets.sideBLove, + width: 24.w, + height: 24.w, + ); + }), + ), ), ), ), - ), - ), + ); + }), + SizedBox(width: 8.w), + Obx(() { + return Visibility( + visible: musicPlayerController.getMusicModel() != null, + child: ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: controller.onTapDownload, + child: Padding( + padding: const EdgeInsets.all(6).w, + child: SizedBox( + width: 24.w, + height: 24.w, + child: Obx(() { + if (OfflineBox().checkDownloaded(musicPlayerController.getMusicModel()?.value.videoId) + || musicPlayerController.getMusicModel()?.value.taskStatus == TaskStatus.complete) { + return Image.asset(Assets.sideBDownloaded); + } else { + if (musicPlayerController.getMusicModel()?.value.taskStatus == TaskStatus.enqueued) { + return CircularProgressIndicator( + color: seedColor, + strokeWidth: 2.w, + ); + } else if (musicPlayerController.getMusicModel()?.value.taskStatus == TaskStatus.running) { + return CircularProgressIndicator( + value: musicPlayerController.getMusicModel()?.value.progress, + backgroundColor: seedColor.withOpacity(0.2), + valueColor: + const AlwaysStoppedAnimation(seedColor), + strokeWidth: 2.w, + ); + } else { + return Image.asset(Assets.sideBNotDownload1); + } + } + }), + ), + ), + ), + ), + ), + ); + }), ], ); } @@ -284,7 +305,7 @@ class PlayPageView extends StatelessWidget { } /// 播放控制按钮 - Widget _buildPlayControlButtons() { + Widget _buildPlayControlButtons(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 10).w, child: Row( @@ -322,15 +343,13 @@ class PlayPageView extends StatelessWidget { 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), - ); - }), + padding: const EdgeInsets.all(10).w, + child: Image.asset( + Assets.sideBPreviousTrack, + width: 20.w, + height: 20.w, + color: Colors.white, + ), ), ), ), @@ -338,16 +357,19 @@ class PlayPageView extends StatelessWidget { 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 + child: Obx(() { + return InkWell( + onTap: musicPlayerController.processingState.value == ProcessingState.ready + || musicPlayerController.processingState.value == ProcessingState.completed + ? musicPlayerController.playPause : null, + child: SizedBox( + width: 66.w, + height: 66.w, + child: FittedBox( + fit: BoxFit.none, + child: Visibility( + visible: musicPlayerController.processingState.value == ProcessingState.buffering + || musicPlayerController.processingState.value == ProcessingState.ready || musicPlayerController.processingState.value == ProcessingState.completed, replacement: Center( child: SizedBox( @@ -364,8 +386,25 @@ class PlayPageView extends StatelessWidget { width: 26.w, height: 26.w, ), - ); - }), + ), + ), + ), + ); + }), + ), + ), + ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: musicPlayerController.nextTrack, + child: Padding( + padding: const EdgeInsets.all(10).w, + child: Image.asset( + Assets.sideBNextTrack, + width: 20.w, + height: 20.w, + color: Colors.white, ), ), ), @@ -375,26 +414,7 @@ class PlayPageView extends StatelessWidget { 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, + onTap: () => _openPlayList(context), child: Padding( padding: const EdgeInsets.all(10).w, child: Image.asset( @@ -411,12 +431,13 @@ class PlayPageView extends StatelessWidget { ); } - void _openPlayList() { + void _openPlayList(BuildContext context) { Get.bottomSheet( - isScrollControlled:true, + isScrollControlled: true, Container( width: 1.sw, height: 0.6.sh, + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), decoration: BoxDecoration( color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.only(topLeft: Radius.circular(18.r), topRight: Radius.circular(18.r)), @@ -439,9 +460,9 @@ class PlayPageView extends StatelessWidget { child: BaseScrollbar( child: Obx(() { return ListView.builder( - itemCount: musicPlayerController.playList.length, + itemCount: musicPlayerController.playlist.length, itemBuilder: (context, index) { - return _buildPlayListItem(index, musicPlayerController.playList[index]); + return _buildPlayListItem(index); }, ); }), @@ -453,18 +474,19 @@ class PlayPageView extends StatelessWidget { ); } - Widget _buildPlayListItem(int index, MusicModel model) { + Widget _buildPlayListItem(int index) { + final musicModel = musicPlayerController.playlist[index].value; return Material( color: Colors.transparent, child: InkWell( - onTap: () => controller.switchMusic(index, model), + onTap: () => controller.switchMusic(musicModel), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8).h, child: Row( children: [ SizedBox(width: 18.w), NetworkImageWidget( - url: model.thumbnail, + url: musicModel.coverUrl, width: 60.w, height: 60.w, radius: 10.r, @@ -475,27 +497,53 @@ class PlayPageView extends StatelessWidget { 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, + return Visibility( + visible: musicPlayerController.getMusicModel()?.value.videoId == musicModel.videoId, + replacement: Text( + ObjUtil.getStr(musicModel.title), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: musicPlayerController.getMusicModel()?.value.videoId == musicModel.videoId + ? seedColor + : const Color(0xD9FFFFFF), + fontSize: 14.sp, + ), + ), + child: MyMarqueeText( + text: ObjUtil.getStr(musicModel.title), + textStyle: TextStyle( + color: musicPlayerController.getMusicModel()?.value.videoId == musicModel.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, + return Visibility( + visible: musicPlayerController.getMusicModel()?.value.videoId == musicModel.videoId, + replacement: Text( + ObjUtil.getStr(musicModel.subtitle), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: musicPlayerController.getMusicModel()?.value.videoId == musicModel.videoId + ? seedColor + : const Color(0x99FFFFFF), + fontSize: 12.sp, + ), + ), + child: MyMarqueeText( + text: ObjUtil.getStr(musicModel.subtitle), + textStyle: TextStyle( + color: musicPlayerController.getMusicModel()?.value.videoId == musicModel.videoId + ? seedColor + : const Color(0x99FFFFFF), + fontSize: 12.sp, + ), ), ); }), @@ -504,14 +552,14 @@ class PlayPageView extends StatelessWidget { ), Obx(() { return Visibility( - visible: musicPlayerController.musicModel.value.videoId != model.videoId, + visible: musicPlayerController.getMusicModel()?.value.videoId != musicModel.videoId, child: ClipOval( child: Material( color: Colors.transparent, child: InkWell( - onTap: () => controller.deleteMusic(index), + onTap: () => controller.deleteMusic(index, musicModel), child: Padding( - padding: const EdgeInsets.all(10).w, + padding: const EdgeInsets.all(6).w, child: Image.asset( Assets.sideBPlayListDelete, width: 24.w, @@ -524,7 +572,7 @@ class PlayPageView extends StatelessWidget { ); }), Obx(() { - return SizedBox(width: musicPlayerController.musicModel.value.videoId != model.videoId ? 8.w : 16.w); + return SizedBox(width: musicPlayerController.getMusicModel()?.value.videoId != musicModel.videoId ? 12.w : 16.w); }), ], ), diff --git a/lib/modules/sideb/playlists/playlists_binding.dart b/lib/modules/sideb/playlists/playlists_binding.dart deleted file mode 100644 index ac75a25..0000000 --- a/lib/modules/sideb/playlists/playlists_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -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 index 3dbdd7d..d152c0b 100644 --- a/lib/modules/sideb/playlists/playlists_controller.dart +++ b/lib/modules/sideb/playlists/playlists_controller.dart @@ -1,12 +1,42 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tone_snap/components/dialog/add_playlist_dialog.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/models/playlist_model.dart'; +import 'package:tone_snap/data/storage/playlists_box.dart'; class PlaylistsController extends GetxController { - ScrollController scrollController = ScrollController(); + static PlaylistsController get to => Get.find(); + var viewState = ViewState.loading.obs; + var scrollController = ScrollController(); + var playLists = [].obs; + + @override + void onReady() { + super.onReady(); + getList(); + } @override void onClose() { scrollController.dispose(); super.onClose(); } + + void getList() { + playLists.value = PlaylistsBox().getReversedList(); + viewState.value = playLists.isNotEmpty ? ViewState.normal : ViewState.empty; + } + + void createPlayList() { + Get.dialog( + barrierDismissible: false, + CreatePlaylistDialog( + onTap: (value) async { + await PlaylistsBox().add(value); + getList(); + }, + ), + ); + } } \ No newline at end of file diff --git a/lib/modules/sideb/playlists/playlists_view.dart b/lib/modules/sideb/playlists/playlists_view.dart index 2aeca76..b658f5a 100644 --- a/lib/modules/sideb/playlists/playlists_view.dart +++ b/lib/modules/sideb/playlists/playlists_view.dart @@ -2,8 +2,10 @@ 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/components/view_state_widget.dart'; import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/sideb/widgets/playlist_item.dart'; +import 'package:tone_snap/modules/sideb/widgets/remove_padding_music_bar.dart'; import 'playlists_controller.dart'; @@ -12,12 +14,13 @@ class PlaylistsView extends GetView { @override Widget build(BuildContext context) { - Get.find(); + Get.put(PlaylistsController()); return Column( children: [ _buildAddPlaylists(), - _buildListView(), + _buildList(), + const RemovePaddingMusicBar(), ], ); } @@ -40,9 +43,9 @@ class PlaylistsView extends GetView { child: Material( color: Colors.transparent, child: InkWell( - onTap: () {}, + onTap: controller.createPlayList, child: Padding( - padding: const EdgeInsets.all(10).w, + padding: const EdgeInsets.all(6).w, child: Image.asset( Assets.sideBPlaylistsAdd, width: 20.w, @@ -52,69 +55,33 @@ class PlaylistsView extends GetView { ), ), ), - SizedBox(width: 6.w), + SizedBox(width: 10.w), ], ); } - Widget _buildListView() { + Widget _buildList() { 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, - ), - ), - ], - ), - ), - ], - ), - ), + child: Obx(() { + return ViewStateWidget( + viewState: controller.viewState.value, + child: BaseScrollbar( + scrollController: controller.scrollController, + child: Obx(() { + return ListView.builder( + controller: controller.scrollController, + itemCount: controller.playLists.length, + itemBuilder: (context, index) { + return PlaylistItem( + index: 0, + playlistModel: controller.playLists[index], + ); + }, + ); + }), + ), + ); + }), ); } } diff --git a/lib/modules/sideb/search_music/search_music_binding.dart b/lib/modules/sideb/search_music/search_music_binding.dart deleted file mode 100644 index bf3c06c..0000000 --- a/lib/modules/sideb/search_music/search_music_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; - -import 'search_music_controller.dart'; - -class SearchMusicBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => SearchMusicController()); - } -} diff --git a/lib/modules/sideb/search_music/search_music_controller.dart b/lib/modules/sideb/search_music/search_music_controller.dart index e6e4993..4dc90d1 100644 --- a/lib/modules/sideb/search_music/search_music_controller.dart +++ b/lib/modules/sideb/search_music/search_music_controller.dart @@ -1,5 +1,32 @@ import 'package:get/get.dart'; +import 'package:tone_snap/data/storage/music_box.dart'; +import 'package:tone_snap/routes/app_routes.dart'; class SearchMusicController extends GetxController { + static SearchMusicController get to => Get.find(); + var historyList = [].obs; + @override + void onReady() { + super.onReady(); + getList(); + } + + void getList() { + historyList.value = MusicBox().getAllSearchHistory(); + update(['history']); + } + + void deleteHistory() { + MusicBox().deleteAllSearchHistory(); + getList(); + } + + void onTapHistoryItem(String history) { + Get.toNamed(AppRoutes.searchResult, arguments: history); + } + + void openSearch() { + Get.toNamed(AppRoutes.searchResult); + } } diff --git a/lib/modules/sideb/search_music/search_music_view.dart b/lib/modules/sideb/search_music/search_music_view.dart index c11e995..7a56176 100644 --- a/lib/modules/sideb/search_music/search_music_view.dart +++ b/lib/modules/sideb/search_music/search_music_view.dart @@ -1,5 +1,8 @@ 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/widgets/remove_padding_music_bar.dart'; import 'search_music_controller.dart'; @@ -8,8 +11,129 @@ class SearchMusicView extends GetView { @override Widget build(BuildContext context) { - Get.find(); + Get.put(SearchMusicController()); - return Container(); + return SafeArea( + child: Column( + children: [ + _buildSearch(), + _buildHistory(), + const Spacer(), + const RemovePaddingMusicBar(), + ], + ), + ); + } + + Widget _buildSearch() { + return GestureDetector( + onTap: controller.openSearch, + child: Container( + height: 32.h, + margin: EdgeInsets.symmetric(vertical: 10.h, horizontal: 18.w), + padding: EdgeInsets.symmetric(horizontal: 18.w), + decoration: BoxDecoration( + color: const Color(0xFF212121), + borderRadius: BorderRadius.circular(20).r, + ), + child: Row( + children: [ + Image.asset( + Assets.sideBSearchWhite, + width: 16.w, + height: 16.w, + ), + SizedBox(width: 10.w), + Text( + 'Search songs,playlists', + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0xFF666666), + fontSize: 14.sp, + ), + ), + ], + ), + ), + ); + } + + Widget _buildHistory() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 46.h, + padding: const EdgeInsets.only(left: 18, right: 12).w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'History', + style: TextStyle( + color: const Color(0xFF666666), + fontSize: 14.sp, + ), + ), + Obx(() { + return Visibility( + visible: controller.historyList.isNotEmpty, + child: ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: controller.deleteHistory, + child: Padding( + padding: const EdgeInsets.all(6).w, + child: Image.asset( + Assets.sideBDeleteHistory, + width: 24.w, + height: 24.w, + ), + ), + ), + ), + ), + ); + }), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18).w, + child: GetBuilder( + id: 'history', + builder: (logic) { + return Wrap( + spacing: 8.w, + runSpacing: 8.h, + children: controller.historyList.map((history) { + return InkWell( + onTap: () => controller.onTapHistoryItem(history), + child: IntrinsicWidth( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h), + decoration: BoxDecoration( + color: const Color(0xFF1F1F1F), + borderRadius: BorderRadius.circular(30).r, + ), + alignment: Alignment.center, + child: Text( + history, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + ), + ), + ), + ), + ); + }).toList(), + ); + }, + ), + ), + ], + ); } } diff --git a/lib/modules/sideb/search_result/search_result_binding.dart b/lib/modules/sideb/search_result/search_result_binding.dart new file mode 100644 index 0000000..cf97d5e --- /dev/null +++ b/lib/modules/sideb/search_result/search_result_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'search_result_controller.dart'; + +class SearchResultBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SearchResultController()); + } +} diff --git a/lib/modules/sideb/search_result/search_result_controller.dart b/lib/modules/sideb/search_result/search_result_controller.dart new file mode 100644 index 0000000..2cca98a --- /dev/null +++ b/lib/modules/sideb/search_result/search_result_controller.dart @@ -0,0 +1,271 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.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/music_type.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/models/search_result_model.dart'; +import 'package:tone_snap/data/models/search_result_tabbar_model.dart'; +import 'package:tone_snap/data/models/search_suggestions_model.dart'; +import 'package:tone_snap/data/storage/music_box.dart'; +import 'package:tone_snap/modules/sideb/search_music/search_music_controller.dart'; +import 'package:tone_snap/modules/sideb/search_result_child/search_result_child_controller.dart'; +import 'package:tone_snap/modules/sideb/search_result_child/search_result_child_view.dart'; +import 'package:tone_snap/modules/sideb/search_result_child_optimum/search_result_child_optimum_controller.dart'; +import 'package:tone_snap/modules/sideb/search_result_child_optimum/search_result_child_optimum_view.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class SearchResultController extends GetxController with GetTickerProviderStateMixin { + static SearchResultController get to => Get.find(); + var textEditingController = TextEditingController(); + final FocusNode myFocusNode = FocusNode(); + var shouldAutofocus = true; + var searchKeyword = ''.obs; + var cancelToken = CancelToken(); + var searchSuggestionsList = [].obs; + var searchSuggestionsViewState = ViewState.normal.obs; + + var tabController = Rx(null); + var tabs = []; + var pages = []; + var searchResultViewState = Rx(null); + + @override + void onReady() { + super.onReady(); + searchKeyword.value = Get.arguments ?? ''; + if (ObjUtil.isNotEmpty(searchKeyword.value)) { + shouldAutofocus = false; + textEditingController.text = searchKeyword.value; + onTapSearch(searchKeyword.value); + } + if (shouldAutofocus) { + myFocusNode.requestFocus(); + } + } + + @override + void onClose() { + myFocusNode.dispose(); + textEditingController.dispose(); + if (tabController.value != null) tabController.value?.dispose(); + super.onClose(); + } + + void onTapCancel() { + Get.back(); + } + + Future onChanged(String value) async { + searchKeyword.value = value; + searchSuggestionsList.clear(); + cancelToken.cancel(); + _cleanTab(); + if (ObjUtil.isEmpty(value)) { + return; + } + searchSuggestionsViewState.value = ViewState.loading; + cancelToken = CancelToken(); + SearchSuggestionsModel? searchSuggestionsModel = await MusicApi.searchSuggestions(input: value, cancelToken: cancelToken); + if (searchSuggestionsModel != null) { + var contents1 = searchSuggestionsModel.contents; + if (contents1 != null && contents1.isNotEmpty) { + var contents2 = contents1[0].searchSuggestionsSectionRenderer?.contents; + if (contents2 != null && contents2.isNotEmpty) { + for (var o in contents2) { + var query = o.searchSuggestionRenderer?.navigationEndpoint?.searchEndpoint?.query; + if (ObjUtil.isNotEmpty(query)) { + searchSuggestionsList.add(query!); + } + } + } + } + } + searchSuggestionsViewState.value = ViewState.normal; + } + + void cleanTextEditingController() { + searchKeyword.value = ''; + textEditingController.clear(); + searchSuggestionsList.clear(); + cancelToken.cancel(); + _cleanTab(); + } + + Future _cleanTab() async { + searchResultViewState.value = null; + if (tabController.value != null) tabController.value?.dispose(); + tabController.value = null; + if (Get.isRegistered()) { + await Get.delete(); + } + for (var o in tabs) { + var tag = '${o.title}-${o.params}'; + if (Get.isRegistered(tag: tag)) { + await Get.delete(tag: tag); + } + } + tabs.clear(); + pages.clear(); + } + + Future onTapSearch(String value, {bool isSearchSuggestionsItem = true}) async { + if (ObjUtil.isEmpty(textEditingController.text)) { + return; + } + if (isSearchSuggestionsItem) { + textEditingController.text = value; + } + await MusicBox().putSearchHistory(textEditingController.text); + if (Get.isRegistered()) { + SearchMusicController.to.getList(); + } + await _cleanTab(); + searchResultViewState.value = ViewState.loading; + await _searchPreviewResult(value); + searchResultViewState.value = tabs.isNotEmpty ? ViewState.normal : ViewState.empty; + tabController.value = TabController(length: tabs.length, vsync: this); + } + + Future _searchPreviewResult(String keyword) async { + Map queryParameters = { + 'prettyPrint': false, + 'query': keyword + }; + SearchResultModel? searchResultModel = await MusicApi.search(queryParameters: queryParameters, formJson: SearchResultModel.fromMap); + if (searchResultModel != null) { + var tabs = searchResultModel.contents?.tabbedSearchResultsRenderer?.tabs; + if (tabs != null && tabs.isNotEmpty) { + var contents1 = tabs[0].tabRenderer?.content?.sectionListRenderer?.contents; + if (contents1 != null && contents1.isNotEmpty) { + for (var o in contents1) { + var searchResultTabBarModel = SearchResultTabBarModel(); + MusicModel? musicModelCard; + if (o.musicCardShelfRenderer != null) { + musicModelCard = MusicModel(); + var runs1 = o.musicCardShelfRenderer?.header?.musicCardShelfHeaderBasicRenderer?.title?.runs; + if (runs1 != null && runs1.isNotEmpty) { + searchResultTabBarModel.title = runs1[0].text; + searchResultTabBarModel.uniqueId = runs1[0].text; + } + var thumbnails = o.musicCardShelfRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails; + if (thumbnails != null && thumbnails.isNotEmpty) { + musicModelCard.coverUrl = thumbnails.last.url; + } + var runs2 = o.musicCardShelfRenderer?.title?.runs; + if (runs2 != null && runs2.isNotEmpty) { + musicModelCard.title = runs2[0].text; + if (runs2[0].navigationEndpoint?.watchEndpoint != null) { + musicModelCard.musicType = runs2[0].navigationEndpoint?.watchEndpoint?.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig?.musicVideoType; + } else if (runs2[0].navigationEndpoint?.browseEndpoint != null) { + musicModelCard.browseId = runs2[0].navigationEndpoint?.browseEndpoint?.browseId; + musicModelCard.musicType = runs2[0].navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType; + } + } + var runs3 = o.musicCardShelfRenderer?.subtitle?.runs; + if (runs3 != null && runs3.isNotEmpty) { + musicModelCard.subtitle = runs3.map((e) => e.text).join(); + } + var items = o.musicCardShelfRenderer?.menu?.menuRenderer?.items; + if (items != null && items.isNotEmpty) { + musicModelCard.playlistId = items[0].menuNavigationItemRenderer?.navigationEndpoint?.watchEndpoint?.playlistId; + } + searchResultTabBarModel.musicList ??= []; + this.tabs.add(searchResultTabBarModel); + } else if (o.musicShelfRenderer != null) { + var runs = o.musicShelfRenderer?.title?.runs; + if (runs != null && runs.isNotEmpty) { + searchResultTabBarModel.title = runs[0].text; + } + var contents2 = o.musicShelfRenderer?.contents; + if (contents2 != null && contents2.isNotEmpty) { + for (var e in contents2) { + var musicModel = MusicModel(); + var thumbnails = e.musicResponsiveListItemRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails; + if (thumbnails != null && thumbnails.isNotEmpty) { + musicModel.coverUrl = thumbnails.last.url; + } + var flexColumns = e.musicResponsiveListItemRenderer?.flexColumns; + if (flexColumns != null && flexColumns.isNotEmpty) { + for (var i = 0; i < flexColumns.length; ++i) { + var m = flexColumns[i]; + var runs = m.musicResponsiveListItemFlexColumnRenderer?.text?.runs; + if (runs != null && runs.isNotEmpty) { + if (i == 0) { + musicModel.title = runs[0].text; + musicModel.videoId = runs[0].navigationEndpoint?.watchEndpoint?.videoId; + musicModel.musicType = runs[0].navigationEndpoint?.watchEndpoint?.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig?.musicVideoType; + } else { + musicModel.subtitle = ObjUtil.getStr(musicModel.subtitle) + runs.map((e) => e.text).join(); + } + } + } + } + var items = e.musicResponsiveListItemRenderer?.menu?.menuRenderer?.items; + if (items != null && items.isNotEmpty) { + musicModel.playlistId = items[0].menuNavigationItemRenderer?.navigationEndpoint?.watchEndpoint?.playlistId; + } + if (e.musicResponsiveListItemRenderer?.navigationEndpoint != null) { + musicModel.browseId = e.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId; + musicModel.musicType = e.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType; + } + searchResultTabBarModel.musicList ??= []; + if (musicModelCard != null && musicModel.musicType == musicModelCard.musicType) { + searchResultTabBarModel.musicList!.add(musicModelCard.copyWith()); + musicModelCard = null; + } + if (musicModel.musicType == MusicType.musicVideoTypeAtv.name + || musicModel.musicType == MusicType.musicPageTypeAlbum.name + || musicModel.musicType == MusicType.musicPageTypePlaylist.name) { + searchResultTabBarModel.musicList!.add(musicModel); + } + } + } + bool isAdd = false; + searchResultTabBarModel.musicList?.map((e) => isAdd = e.musicType == MusicType.musicVideoTypeAtv.name + || e.musicType == MusicType.musicPageTypeAlbum.name + || e.musicType == MusicType.musicPageTypePlaylist.name).toList(); + if (isAdd) { + this.tabs.add(searchResultTabBarModel); + } + } + } + } + + var chips = tabs[0].tabRenderer?.content?.sectionListRenderer?.header?.chipCloudRenderer?.chips; + if (chips != null && chips.isNotEmpty) { + for (var o in chips) { + var searchResultTabBarModel = SearchResultTabBarModel(); + var runs = o.chipCloudChipRenderer?.text?.runs; + if (runs != null && runs.isNotEmpty) { + searchResultTabBarModel.title = runs[0].text; + } + searchResultTabBarModel.params = o.chipCloudChipRenderer?.navigationEndpoint?.searchEndpoint?.params; + searchResultTabBarModel.uniqueId = o.chipCloudChipRenderer?.uniqueId; + + for (var e in this.tabs) { + if (e.title == searchResultTabBarModel.title) { + e.uniqueId = searchResultTabBarModel.uniqueId; + e.params = searchResultTabBarModel.params; + if (e.musicList != null) { + for (var o in e.musicList!) { + o.params = searchResultTabBarModel.params; + } + } + break; + } + } + } + for (var i = 0; i < this.tabs.length; ++i) { + if (i == 0) { + pages.add(const SearchResultChildOptimumView()); + } else { + pages.add(SearchResultChildView(searchResultTabBarModel: this.tabs[i])); + } + } + } + } + } + } +} diff --git a/lib/modules/sideb/search_result/search_result_view.dart b/lib/modules/sideb/search_result/search_result_view.dart new file mode 100644 index 0000000..57160da --- /dev/null +++ b/lib/modules/sideb/search_result/search_result_view.dart @@ -0,0 +1,211 @@ +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/view_state_widget.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/sideb/widgets/remove_padding_music_bar.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +import 'search_result_controller.dart'; + +class SearchResultView extends StatelessWidget { + SearchResultView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + children: [ + _buildSearch(context), + Obx(() { + if (controller.searchResultViewState.value != null) { + return _buildSearchResult(); + } else { + return _buildSearchSuggestions(); + } + }), + const RemovePaddingMusicBar(), + ], + ), + ), + ); + } + + Widget _buildSearch(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 18.w), + child: Row( + children: [ + Expanded( + child: Container( + height: 40.h, + padding: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(20).r, + ), + child: Row( + children: [ + Image.asset( + Assets.sideBSearchWhite, + width: 24.w, + height: 24.w, + ), + Expanded( + child: TextField( + maxLines: 1, + focusNode: controller.myFocusNode, + controller: controller.textEditingController, + textInputAction: TextInputAction.done, + keyboardType: TextInputType.text, + style: TextStyle(color: const Color(0xD9FFFFFF), fontSize: 14.sp), + textAlignVertical: TextAlignVertical.center, + textAlign: TextAlign.left, + maxLength: 60, + decoration: InputDecoration( + counterText: '', + hintText: 'Search songs,playlists', + hintStyle: TextStyle( + color: const Color(0xFF666666), + fontSize: 14.sp, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 14).w, + isCollapsed: true, + border: InputBorder.none, + ), + onChanged: (value) => controller.onChanged(value), + onSubmitted: (value) => controller.onTapSearch(value, isSearchSuggestionsItem: false), + ), + ), + Obx(() { + return Visibility( + visible: ObjUtil.isNotEmpty(controller.searchKeyword.value), + child: GestureDetector( + onTap: controller.cleanTextEditingController, + child: Image.asset( + Assets.sideBDeleteWhite, + width: 24.w, + height: 24.w, + ), + ), + ); + }), + ], + ), + ), + ), + SizedBox(width: 18.w), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: controller.onTapCancel, + child: Container( + height: 40.h, + alignment: Alignment.center, + child: Text( + 'Cancel', + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0xD9FFFFFF), + fontSize: 14.sp, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildSearchSuggestions() { + return Expanded( + child: Obx(() { + return ViewStateWidget( + viewState: controller.searchSuggestionsViewState.value, + child: Obx(() { + return ListView.builder( + itemCount: controller.searchSuggestionsList.length, + itemBuilder: (context, index) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => controller.onTapSearch(controller.searchSuggestionsList[index]), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 14.h), + child: Text( + controller.searchSuggestionsList[index], + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + ), + ), + ); + }, + ); + }), + ); + }), + ); + } + + Widget _buildSearchResult() { + return Expanded( + child: Obx(() { + return ViewStateWidget( + viewState: controller.searchResultViewState.value!, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 46.h, + child: Obx(() { + return TabBar( + controller: controller.tabController.value, + isScrollable: true, + tabAlignment: TabAlignment.start, + dividerHeight: 0, + padding: const EdgeInsets.symmetric(horizontal: 6).w, + labelPadding: const EdgeInsets.symmetric(horizontal: 12).w, + labelStyle: TextStyle( + color: seedColor, + fontSize: 16.sp, + ), + unselectedLabelStyle: TextStyle( + color: const Color(0xFF666666), + fontSize: 16.sp, + ), + indicatorPadding: const EdgeInsets.only(bottom: 4).h, + indicator: MyCustomIndicator( + indWidth: 16.w, + indHeight: 3.h, + color: seedColor, + radius: 20.r, + ), + tabs: controller.tabs.map((e) => Tab(text: e.title)).toList(), + ); + }), + ), + Expanded( + child: Obx(() { + return TabBarView( + controller: controller.tabController.value, + children: controller.pages.map((e) => KeepAliveWrapper(child: e)).toList(), + ); + }), + ), + ], + ), + ); + }), + ); + } +} diff --git a/lib/modules/sideb/search_result_child/search_result_child_controller.dart b/lib/modules/sideb/search_result_child/search_result_child_controller.dart new file mode 100644 index 0000000..5d91d43 --- /dev/null +++ b/lib/modules/sideb/search_result_child/search_result_child_controller.dart @@ -0,0 +1,203 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.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/music_type.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/models/search_result_model.dart'; +import 'package:tone_snap/data/models/search_result_more_model.dart'; +import 'package:tone_snap/modules/sideb/search_result/search_result_controller.dart'; +import 'package:tone_snap/routes/app_routes.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class SearchResultChildController extends GetxController { + var viewState = ViewState.loading.obs; + var scrollController = ScrollController(); + var musicList = >[].obs; + String? params; + String? continuation; + String? clickTrackingParams; + var refreshController = EasyRefreshController( + controlFinishLoad: true, + ); + + @override + void onReady() { + super.onReady(); + _searchResult(); + } + + @override + void onClose() { + refreshController.dispose(); + scrollController.dispose(); + super.onClose(); + } + + void setParams(String? params) { + this.params = params; + } + + void onLoad() { + _searchResultLoadMore(); + } + + Future _searchResult() async { + Map queryParameters = { + 'prettyPrint': false, + 'query': SearchResultController.to.searchKeyword, + 'params': params + }; + SearchResultModel? searchResultModel = await MusicApi.search(queryParameters: queryParameters, formJson: SearchResultModel.fromMap); + final list = []; + if (searchResultModel != null) { + var tabs = searchResultModel.contents?.tabbedSearchResultsRenderer?.tabs; + if (tabs != null && tabs.isNotEmpty) { + var contents1 = tabs[0].tabRenderer?.content?.sectionListRenderer?.contents; + if (contents1 != null && contents1.isNotEmpty) { + var continuations = contents1[0].musicShelfRenderer?.continuations; + if (continuations != null && continuations.isNotEmpty) { + continuation = continuations[0].nextContinuationData?.continuation; + clickTrackingParams = continuations[0].nextContinuationData?.clickTrackingParams; + } + for (var o in contents1) { + if (o.musicShelfRenderer != null) { + var contents2 = o.musicShelfRenderer?.contents; + if (contents2 != null && contents2.isNotEmpty) { + for (var e in contents2) { + var musicModel = MusicModel(); + var thumbnails = e.musicResponsiveListItemRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails; + if (thumbnails != null && thumbnails.isNotEmpty) { + musicModel.coverUrl = thumbnails.last.url; + } + var flexColumns = e.musicResponsiveListItemRenderer?.flexColumns; + if (flexColumns != null && flexColumns.isNotEmpty) { + for (var i = 0; i < flexColumns.length; ++i) { + var m = flexColumns[i]; + var runs = m.musicResponsiveListItemFlexColumnRenderer?.text?.runs; + if (runs != null && runs.isNotEmpty) { + if (i == 0) { + musicModel.title = runs[0].text; + musicModel.videoId = runs[0].navigationEndpoint?.watchEndpoint?.videoId; + musicModel.musicType = runs[0].navigationEndpoint?.watchEndpoint?.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig?.musicVideoType; + } else { + musicModel.subtitle = ObjUtil.getStr(musicModel.subtitle) + runs.map((e) => e.text).join(); + } + } + } + } + var items = e.musicResponsiveListItemRenderer?.menu?.menuRenderer?.items; + if (items != null && items.isNotEmpty) { + musicModel.playlistId = items[0].menuNavigationItemRenderer?.navigationEndpoint?.watchEndpoint?.playlistId; + } + if (e.musicResponsiveListItemRenderer?.navigationEndpoint != null) { + musicModel.browseId = e.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId; + musicModel.musicType = e.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType; + } + if (musicModel.musicType == MusicType.musicVideoTypeAtv.name + || musicModel.musicType == MusicType.musicPageTypeAlbum.name + || musicModel.musicType == MusicType.musicPageTypePlaylist.name) { + list.add(musicModel); + } + } + } + } + } + } + } + } + musicList.value = list.map((e) => e.obs).toList(); + viewState.value = musicList.isNotEmpty ? ViewState.normal : ViewState.empty; + } + + Future _searchResultLoadMore() async { + if (ObjUtil.isEmpty(continuation)) { + return; + } + Map queryParameters = { + 'ctoken': continuation, + 'continuation': continuation, + 'type': 'next', + 'itct': clickTrackingParams, + 'prettyPrint': false + }; + refreshController.callLoad(); + SearchResultMoreModel? searchResultMoreModel = await MusicApi.search(queryParameters: queryParameters, formJson: SearchResultMoreModel.fromMap); + final list = []; + if (searchResultMoreModel != null) { + var continuations = searchResultMoreModel.continuationContents?.musicShelfContinuation?.continuations; + if (continuations != null && continuations.isNotEmpty) { + continuation = continuations[0].nextContinuationData?.continuation; + clickTrackingParams = continuations[0].nextContinuationData?.clickTrackingParams; + } else { + continuation = null; + clickTrackingParams = null; + } + var contents = searchResultMoreModel.continuationContents?.musicShelfContinuation?.contents; + if (contents != null && contents.isNotEmpty) { + for (var o in contents) { + var musicModel = MusicModel(); + var thumbnails = o.musicResponsiveListItemRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails; + if (thumbnails != null && thumbnails.isNotEmpty) { + musicModel.coverUrl = thumbnails.last.url; + } + var flexColumns = o.musicResponsiveListItemRenderer?.flexColumns; + if (flexColumns != null && flexColumns.isNotEmpty) { + for (var i = 0; i < flexColumns.length; ++i) { + var m = flexColumns[i]; + var runs = m.musicResponsiveListItemFlexColumnRenderer?.text?.runs; + if (runs != null && runs.isNotEmpty) { + if (i == 0) { + musicModel.title = runs[0].text; + musicModel.videoId = runs[0].navigationEndpoint?.watchEndpoint?.videoId; + musicModel.musicType = runs[0].navigationEndpoint?.watchEndpoint?.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig?.musicVideoType; + } else { + musicModel.subtitle = ObjUtil.getStr(musicModel.subtitle) + runs.map((e) => e.text).join(); + } + } + } + } + var items = o.musicResponsiveListItemRenderer?.menu?.menuRenderer?.items; + if (items != null && items.isNotEmpty) { + musicModel.playlistId = items[0].menuNavigationItemRenderer?.navigationEndpoint?.watchEndpoint?.playlistId; + } + if (o.musicResponsiveListItemRenderer?.navigationEndpoint != null) { + musicModel.browseId = o.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId; + musicModel.musicType = o.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType; + } + list.add(musicModel); + } + } + } + musicList.addAll(list.map((e) => e.obs).toList()); + if (searchResultMoreModel != null) { + if (ObjUtil.isNotEmpty(continuation)) { + refreshController.finishLoad(IndicatorResult.success); + } else { + refreshController.finishLoad(IndicatorResult.noMore); + } + } else { + refreshController.finishLoad(IndicatorResult.fail); + } + viewState.value = musicList.isNotEmpty ? ViewState.normal : ViewState.empty; + } + + void onTapItem(MusicModel musicModel) { + if (musicModel.musicType == MusicType.musicVideoTypeAtv.name) { + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': musicModel.videoId, + 'playlistId': musicModel.playlistId, + }); + } else if (musicModel.musicType == MusicType.musicPageTypeAlbum.name + || musicModel.musicType == MusicType.musicPageTypePlaylist.name) { + Get.toNamed(AppRoutes.albumSongList, arguments: { + 'browseId': musicModel.browseId, + 'params': musicModel.params, + 'coverUrl': musicModel.coverUrl, + 'title': musicModel.title, + 'subtitle': musicModel.subtitle, + }); + } + } +} diff --git a/lib/modules/sideb/search_result_child/search_result_child_view.dart b/lib/modules/sideb/search_result_child/search_result_child_view.dart new file mode 100644 index 0000000..ea12283 --- /dev/null +++ b/lib/modules/sideb/search_result_child/search_result_child_view.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_scrollbar.dart'; +import 'package:tone_snap/components/refresh/base_easyrefresh.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/enum/music_type.dart'; +import 'package:tone_snap/modules/sideb/widgets/music_item.dart'; + +import '../../../data/models/search_result_tabbar_model.dart'; +import 'search_result_child_controller.dart'; + +class SearchResultChildView extends GetView { + const SearchResultChildView({super.key, required this.searchResultTabBarModel}); + + final SearchResultTabBarModel searchResultTabBarModel; + + @override + String? get tag => '${searchResultTabBarModel.title}-${searchResultTabBarModel.params}'; + + @override + Widget build(BuildContext context) { + Get.put(SearchResultChildController(), tag: tag); + controller.setParams(searchResultTabBarModel.params); + + return Obx(() { + return ViewStateWidget( + viewState: controller.viewState.value, + child: BaseEasyRefresh( + controller: controller.refreshController, + onLoad: controller.onLoad, + child: BaseScrollbar( + scrollController: controller.scrollController, + child: Obx(() { + return ListView.builder( + controller: controller.scrollController, + itemCount: controller.musicList.length, + itemBuilder: (context, index) { + return MusicItem( + musicModel: controller.musicList[index], + showDownload: controller.musicList[index].value.musicType == MusicType.musicVideoTypeAtv.name, + showMore: controller.musicList[index].value.musicType == MusicType.musicVideoTypeAtv.name, + onTapItem: () => controller.onTapItem(controller.musicList[index].value), + ); + }, + ); + }), + ), + ), + ); + }); + } +} diff --git a/lib/modules/sideb/search_result_child_optimum/search_result_child_optimum_controller.dart b/lib/modules/sideb/search_result_child_optimum/search_result_child_optimum_controller.dart new file mode 100644 index 0000000..0d615b5 --- /dev/null +++ b/lib/modules/sideb/search_result_child_optimum/search_result_child_optimum_controller.dart @@ -0,0 +1,24 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/data/enum/music_type.dart'; +import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class SearchResultChildOptimumController extends GetxController { + void onTapItem(MusicModel musicModel) { + if (musicModel.musicType == MusicType.musicVideoTypeAtv.name) { + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': musicModel.videoId, + 'playlistId': musicModel.playlistId, + }); + } else if (musicModel.musicType == MusicType.musicPageTypeAlbum.name + || musicModel.musicType == MusicType.musicPageTypePlaylist.name) { + Get.toNamed(AppRoutes.albumSongList, arguments: { + 'browseId': musicModel.browseId, + 'params': musicModel.params, + 'coverUrl': musicModel.coverUrl, + 'title': musicModel.title, + 'subtitle': musicModel.subtitle, + }); + } + } +} diff --git a/lib/modules/sideb/search_result_child_optimum/search_result_child_optimum_view.dart b/lib/modules/sideb/search_result_child_optimum/search_result_child_optimum_view.dart new file mode 100644 index 0000000..b3623bc --- /dev/null +++ b/lib/modules/sideb/search_result_child_optimum/search_result_child_optimum_view.dart @@ -0,0 +1,69 @@ +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/enum/music_type.dart'; +import 'package:tone_snap/modules/sideb/search_result/search_result_controller.dart'; +import 'package:tone_snap/modules/sideb/search_result_child_optimum/search_result_child_optimum_controller.dart'; +import 'package:tone_snap/modules/sideb/widgets/music_item.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class SearchResultChildOptimumView extends GetView { + const SearchResultChildOptimumView({super.key}); + + @override + Widget build(BuildContext context) { + Get.put(SearchResultChildOptimumController()); + + return BaseScrollbar( + child: ListView.builder( + itemCount: SearchResultController.to.tabs.length, + itemBuilder: (context, index) { + final searchResultTabBarModel = SearchResultController.to.tabs[index]; + final musicList = searchResultTabBarModel.musicList?.map((e) => e.obs).toList(); + if (index == 0) { + return Container(); + } + return Padding( + padding: const EdgeInsets.symmetric(vertical: 14).h, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18).w, + child: Text( + ObjUtil.getStr(searchResultTabBarModel.title), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + ), + ), + ), + if (musicList != null) ...[ + SizedBox(height: 12.h), + SizedBox( + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: musicList.length, + itemBuilder: (context, index) { + return MusicItem( + musicModel: musicList[index], + showDownload: musicList[index].value.musicType == MusicType.musicVideoTypeAtv.name, + showMore: musicList[index].value.musicType == MusicType.musicVideoTypeAtv.name, + onTapItem: () => controller.onTapItem(musicList[index].value), + ); + }, + ), + ), + ], + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/modules/sidea/settings/settings_binding.dart b/lib/modules/sideb/settings/settings_binding.dart similarity index 67% rename from lib/modules/sidea/settings/settings_binding.dart rename to lib/modules/sideb/settings/settings_binding.dart index fb2321e..7570a4c 100644 --- a/lib/modules/sidea/settings/settings_binding.dart +++ b/lib/modules/sideb/settings/settings_binding.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; -import 'package:tone_snap/modules/sidea/settings/settings_controller.dart'; + +import 'settings_controller.dart'; class SettingsBinding extends Bindings { @override diff --git a/lib/modules/sideb/settings/settings_controller.dart b/lib/modules/sideb/settings/settings_controller.dart new file mode 100644 index 0000000..e6fd60d --- /dev/null +++ b/lib/modules/sideb/settings/settings_controller.dart @@ -0,0 +1,22 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class SettingsController extends GetxController { + final options = ['Privacy Policy', 'Terms of Service']; + final optionIcons = [Assets.sideBPrivacyPolicy, Assets.sideBTermsOfService]; + + void onTapItem(int index) async { + if (index == 0) { + Get.toNamed(AppRoutes.privacy, arguments: { + 'title': options[index], + 'url': 'https://tonesnap-privacy.mystrikingly.com', + }); + } else if (index == 1) { + Get.toNamed(AppRoutes.terms, arguments: { + 'title': options[index], + 'url': 'https://tonesnap-terms.mystrikingly.com' + }); + } + } +} \ No newline at end of file diff --git a/lib/modules/sideb/settings/settings_view.dart b/lib/modules/sideb/settings/settings_view.dart new file mode 100644 index 0000000..1aff2c4 --- /dev/null +++ b/lib/modules/sideb/settings/settings_view.dart @@ -0,0 +1,100 @@ +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/widgets/music_appbar.dart'; +import 'package:tone_snap/modules/sideb/widgets/remove_padding_music_bar.dart'; + +import 'settings_controller.dart'; + +class SettingsView extends StatelessWidget { + SettingsView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Stack( + children: [ + _buildPageBg(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MusicAppbar( + title: 'Setting', + ), + _buildList(), + const RemovePaddingMusicBar(), + ], + ), + ], + ), + ); + } + + Widget _buildPageBg() { + return Image.asset( + Assets.sideBSettingBg, + width: 1.sw, + height: 1.sh, + fit: BoxFit.cover, + ); + } + + Widget _buildList() { + return Expanded( + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 20).h, + itemCount: controller.options.length, + itemBuilder: (context, index) { + return _buildItem(index); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 10.h); + }, + ), + ); + } + + Widget _buildItem(index) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => controller.onTapItem(index), + child: Container( + height: 56.h, + padding: const EdgeInsets.only(left: 21, right: 16).w, + child: Row( + children: [ + Image.asset( + controller.optionIcons[index], + width: 32.w, + height: 32.w, + ), + SizedBox(width: 10.w), + Expanded( + child: Text( + controller.options[index], + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + Image.asset( + Assets.sideAIconChevronRight, + width: 18.w, + height: 18.w, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/sideb/widgets/atv_item.dart b/lib/modules/sideb/widgets/atv_item.dart deleted file mode 100644 index 817d6b6..0000000 --- a/lib/modules/sideb/widgets/atv_item.dart +++ /dev/null @@ -1,94 +0,0 @@ -// Author: fengshengxiong -// Date: 2024/6/21 -// Description: 单曲Item - -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/models/home_model.dart'; -import 'package:tone_snap/generated/assets.dart'; -import 'package:tone_snap/utils/obj_util.dart'; - -class AtvItem extends StatelessWidget { - const AtvItem({super.key, required this.content, required this.onTap}); - - final Content content; - final Function() onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: ClipRRect( - borderRadius: BorderRadius.circular(16).r, - child: SizedBox( - width: 168.w, - height: double.infinity, - child: Stack( - children: [ - NetworkImageWidget( - url: content.thumbnail, - width: double.infinity, - height: double.infinity, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - width: double.infinity, - height: 50.h, - color: const Color(0xB3000000), - child: ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 31.0, sigmaY: 31.0), - child: Padding( - padding: const EdgeInsets.only(left: 12, right: 14).w, - child: Row( - children: [ - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - ObjUtil.getStr(content.title), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w600, - ), - ), - Text( - ObjUtil.getStr(content.subTitle), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: const Color(0xFFA3A3A3), - fontSize: 11.sp, - ), - ), - ], - ), - ), - Image.asset( - Assets.sideBItemPlayer1, - width: 30.w, - height: 22.h, - ), - ], - ), - ), - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/modules/sideb/widgets/album_item.dart b/lib/modules/sideb/widgets/browse_item_album_song_list.dart similarity index 73% rename from lib/modules/sideb/widgets/album_item.dart rename to lib/modules/sideb/widgets/browse_item_album_song_list.dart index 8780045..5deda7b 100644 --- a/lib/modules/sideb/widgets/album_item.dart +++ b/lib/modules/sideb/widgets/browse_item_album_song_list.dart @@ -1,17 +1,17 @@ // Author: fengshengxiong // Date: 2024/6/21 -// Description: 专辑Item +// Description: 首页专辑/歌单Item 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/models/home_model.dart'; +import 'package:tone_snap/data/models/music_model.dart'; import 'package:tone_snap/utils/obj_util.dart'; -class AlbumItem extends StatelessWidget { - const AlbumItem({super.key, required this.content, required this.onTap}); +class BrowseItemAlbumSongList extends StatelessWidget { + const BrowseItemAlbumSongList({super.key, required this.musicModel, required this.onTap}); - final Content content; + final MusicModel musicModel; final Function() onTap; @override @@ -20,19 +20,19 @@ class AlbumItem extends StatelessWidget { onTap: onTap, child: SizedBox( width: 109.w, - height: double.infinity, + height: 160.h, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ NetworkImageWidget( - url: content.thumbnail, + url: musicModel.coverUrl, width: 109.w, height: 109.w, radius: 16.r, ), const Spacer(), Text( - ObjUtil.getStr(content.title), + ObjUtil.getStr(musicModel.title), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -42,7 +42,7 @@ class AlbumItem extends StatelessWidget { ), SizedBox(height: 2.h), Text( - ObjUtil.getStr(content.subTitle), + ObjUtil.getStr(musicModel.subtitle), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( diff --git a/lib/modules/sideb/widgets/browse_item_atv.dart b/lib/modules/sideb/widgets/browse_item_atv.dart new file mode 100644 index 0000000..c1dd4dd --- /dev/null +++ b/lib/modules/sideb/widgets/browse_item_atv.dart @@ -0,0 +1,111 @@ +// Author: fengshengxiong +// Date: 2024/6/21 +// Description: 首页单曲Item + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.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/more_bottom_sheet/more_bottom_sheet_view.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class BrowseItemAtv extends StatelessWidget { + const BrowseItemAtv({super.key, required this.musicModel, required this.onTap}); + + final MusicModel musicModel; + final Function() onTap; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: SizedBox( + height: 60.w, + child: Row( + children: [ + _buildCover(), + _buildContent(), + _buildMore(), + ], + ), + ), + ), + ); + } + + Widget _buildCover() { + return NetworkImageWidget( + url: musicModel.coverUrl, + width: 60.w, + height: 60.w, + radius: 8.r, + ); + } + + Widget _buildContent() { + return Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 12).w, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ObjUtil.getStr(musicModel.title), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + SizedBox(height: 4.h), + Text( + ObjUtil.getStr(musicModel.subtitle), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0xFF666666), + fontSize: 12.sp, + ), + ), + ], + ), + ), + ); + } + + Widget _buildMore() { + return Padding( + padding: const EdgeInsets.only(right: 12).w, + child: ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTapMore, + child: Padding( + padding: const EdgeInsets.all(4).w, + child: Image.asset( + Assets.sideBMore, + width: 24.w, + height: 24.w, + ), + ), + ), + ), + ), + ); + } + + void onTapMore() { + Get.bottomSheet( + MoreBottomSheetView( + musicModel: musicModel.obs, + ), + ); + } +} diff --git a/lib/modules/sideb/widgets/omv_item.dart b/lib/modules/sideb/widgets/browse_item_omv.dart similarity index 77% rename from lib/modules/sideb/widgets/omv_item.dart rename to lib/modules/sideb/widgets/browse_item_omv.dart index dd97f0c..661894d 100644 --- a/lib/modules/sideb/widgets/omv_item.dart +++ b/lib/modules/sideb/widgets/browse_item_omv.dart @@ -1,17 +1,17 @@ // Author: fengshengxiong // Date: 2024/6/21 -// Description: 专辑Item +// Description: 首页视频Item 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/models/home_model.dart'; +import 'package:tone_snap/data/models/music_model.dart'; import 'package:tone_snap/utils/obj_util.dart'; -class OmvItem extends StatelessWidget { - const OmvItem({super.key, required this.content}); +class BrowseItemOmv extends StatelessWidget { + const BrowseItemOmv({super.key, required this.musicModel}); - final Content content; + final MusicModel musicModel; @override Widget build(BuildContext context) { @@ -24,14 +24,14 @@ class OmvItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ NetworkImageWidget( - url: content.thumbnail, + url: musicModel.coverUrl, width: 339.w, height: 187.w, radius: 16.r, ), const Spacer(), Text( - ObjUtil.getStr(content.title), + ObjUtil.getStr(musicModel.title), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -41,7 +41,7 @@ class OmvItem extends StatelessWidget { ), SizedBox(height: 2.h), Text( - ObjUtil.getStr(content.subTitle), + ObjUtil.getStr(musicModel.subtitle), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( diff --git a/lib/modules/sideb/widgets/music_appbar.dart b/lib/modules/sideb/widgets/music_appbar.dart index 544a2de..591668c 100644 --- a/lib/modules/sideb/widgets/music_appbar.dart +++ b/lib/modules/sideb/widgets/music_appbar.dart @@ -106,7 +106,7 @@ class MusicAppbar extends StatelessWidget { overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white, - fontSize: 20.sp, + fontSize: 18.sp, ), ) : titleWidget!, ); @@ -120,7 +120,7 @@ class MusicAppbar extends StatelessWidget { child: InkWell( onTap: actionOnTap, child: Padding( - padding: EdgeInsets.all(8.w), + padding: const EdgeInsets.all(6).w, child: action, ), ), diff --git a/lib/modules/sideb/widgets/music_item.dart b/lib/modules/sideb/widgets/music_item.dart new file mode 100644 index 0000000..c0ad8d7 --- /dev/null +++ b/lib/modules/sideb/widgets/music_item.dart @@ -0,0 +1,229 @@ +import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/components/dialog/remind_dialog.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/data/storage/offline_box.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/global/download_manager.dart'; +import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart'; +import 'package:tone_snap/modules/sideb/more_bottom_sheet/more_bottom_sheet_view.dart'; +import 'package:tone_snap/modules/sideb/offline/offline_controller.dart'; +import 'package:tone_snap/modules/sideb/personal_music_library/personal_music_library_controller.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class MusicItem extends StatelessWidget { + MusicItem({ + super.key, + required this.musicModel, + required this.onTapItem, + this.showDownload = true, + this.showMore = true, + this.playlistModelId, + }); + + final musicPlayerController = MusicPlayerController.to; + + final Rx musicModel; + final Function() onTapItem; + final bool showDownload; + final bool showMore; + final String? playlistModelId; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTapItem, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8.h), + child: Row( + children: [ + SizedBox(width: 18.w), + _buildCover(), + SizedBox(width: 12.w), + _buildContent(), + SizedBox(width: 8.w), + if (showDownload) ...[ + _buildDownload(), + SizedBox(width: 4.w), + ], + if (showMore) ...[ + _buildMore(), + SizedBox(width: 4.w), + ], + SizedBox(width: 10.w), + ], + ), + ), + ), + ); + } + + Widget _buildCover() { + return Obx(() { + return NetworkImageWidget( + url: musicModel.value.coverUrl, + width: 60.w, + height: 60.w, + radius: 8.r, + ); + }); + } + + Widget _buildContent() { + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + bool isCurrentPlayModel = ObjUtil.isNotEmpty(musicModel.value.videoId) && musicPlayerController.getMusicModel()?.value.videoId == musicModel.value.videoId; + return Visibility( + visible: isCurrentPlayModel, + replacement: Text( + ObjUtil.getStr(musicModel.value.title), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + child: MyMarqueeText( + text: ObjUtil.getStr(musicModel.value.title), + textStyle: TextStyle( + color: seedColor, + fontSize: 14.sp, + ), + ), + ); + }), + SizedBox(height: 4.h), + Obx(() { + bool isCurrentPlayModel = ObjUtil.isNotEmpty(musicModel.value.videoId) && musicPlayerController.getMusicModel()?.value.videoId == musicModel.value.videoId; + return Visibility( + visible: isCurrentPlayModel, + replacement: Text( + ObjUtil.getStr(musicModel.value.subtitle), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0x99FFFFFF), + fontSize: 12.sp, + ), + ), + child: MyMarqueeText( + text: ObjUtil.getStr(musicModel.value.subtitle), + textStyle: TextStyle( + color: seedColor, + fontSize: 12.sp, + ), + ), + ); + }), + ], + ), + ); + } + + Widget _buildDownload() { + return ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTapDownload, + child: Padding( + padding: const EdgeInsets.all(4).w, + child: SizedBox( + width: 24.w, + height: 24.w, + child: Obx(() { + if (OfflineBox().checkDownloaded(musicModel.value.videoId) || musicModel.value.taskStatus == TaskStatus.complete) { + return Image.asset(Assets.sideBDownloaded); + } else { + if (musicModel.value.taskStatus == TaskStatus.enqueued) { + return CircularProgressIndicator( + color: seedColor, + strokeWidth: 2.w, + ); + } else if (musicModel.value.taskStatus == TaskStatus.running) { + return CircularProgressIndicator( + value: musicModel.value.progress, + backgroundColor: seedColor.withOpacity(0.2), + valueColor: const AlwaysStoppedAnimation(seedColor), + strokeWidth: 2.w, + ); + } else { + return Image.asset(Assets.sideBNotDownload2); + } + } + }), + ), + ), + ), + ), + ); + } + + Widget _buildMore() { + return ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTapMore, + child: Padding( + padding: const EdgeInsets.all(4).w, + child: Image.asset( + Assets.sideBMore, + width: 24.w, + height: 24.w, + ), + ), + ), + ), + ); + } + + void onTapDownload() { + if (OfflineBox().checkDownloaded(musicModel.value.videoId) || musicModel.value.taskStatus == TaskStatus.complete) { + Get.dialog( + RemindDialog( + content: 'Confirm to remove this song?', + confirmOnTap: () async { + await OfflineBox().delete(musicModel.value.videoId!); + musicModel.update((fn) => fn?.taskStatus = null); + BaseEasyLoading.toast('Removed'); + if (Get.isRegistered()) { + PersonalMusicLibraryController.to.refreshOffline(); + } + if (Get.isRegistered()) { + OfflineController.to.getOfflineList(); + } + }, + ), + ); + } else { + if (musicModel.value.taskStatus == TaskStatus.enqueued + || musicModel.value.taskStatus == TaskStatus.running) { + DownloadManager().cancelDownload(musicModel); + } else { + DownloadManager().downloadMusic(musicModel); + } + } + } + + void onTapMore() { + Get.bottomSheet( + MoreBottomSheetView( + musicModel: musicModel, + playlistModelId: playlistModelId, + ), + ); + } +} diff --git a/lib/modules/sideb/widgets/playlist_item.dart b/lib/modules/sideb/widgets/playlist_item.dart new file mode 100644 index 0000000..dbf2cd8 --- /dev/null +++ b/lib/modules/sideb/widgets/playlist_item.dart @@ -0,0 +1,84 @@ +// Author: fengshengxiong +// Date: 2024/6/21 +// Description: 自定义/收藏播放列表Item + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/network_image_widget.dart'; +import 'package:tone_snap/data/models/playlist_model.dart'; +import 'package:tone_snap/data/storage/playlists_box.dart'; +import 'package:tone_snap/routes/app_routes.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class PlaylistItem extends StatelessWidget { + const PlaylistItem({super.key, required this.playlistModel, required this.index, this.onTapItem}); + + final int index; + final PlaylistModel playlistModel; + final Function()? onTapItem; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTapItem ?? _onTapItem, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 16.w), + child: Row( + children: [ + NetworkImageWidget( + url: index == 0 ? PlaylistsBox().getFirstCoverUrl(playlistModel.id) : playlistModel.coverUrl, + width: 60.w, + height: 60.w, + radius: 8.r, + ), + SizedBox(width: 14.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ObjUtil.getStr(playlistModel.title), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + ), + ), + SizedBox(height: 4.h), + Text( + index == 0 ? '${playlistModel.musicList?.length} songs' : ObjUtil.getStr(playlistModel.subtitle), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0xFF999999), + fontSize: 12.sp, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + void _onTapItem() { + if (index == 0) { + Get.toNamed(AppRoutes.customPlaylist, arguments: playlistModel.id); + } else { + Get.toNamed(AppRoutes.albumSongList, arguments: { + 'browseId': playlistModel.id, + 'params': playlistModel.params, + 'coverUrl': playlistModel.coverUrl, + 'title': playlistModel.title, + 'subtitle': playlistModel.subtitle, + }); + } + } +} diff --git a/lib/modules/sideb/widgets/remove_padding_music_bar.dart b/lib/modules/sideb/widgets/remove_padding_music_bar.dart new file mode 100644 index 0000000..fcffb39 --- /dev/null +++ b/lib/modules/sideb/widgets/remove_padding_music_bar.dart @@ -0,0 +1,29 @@ +// Author: fengshengxiong +// Date: 2024/6/21 +// Description: RemovePaddingMusicBar + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/modules/sideb/music_bar/music_bar.dart'; +import 'package:tone_snap/res/themes/app_sizes.dart'; + +class RemovePaddingMusicBar extends StatelessWidget { + const RemovePaddingMusicBar({super.key}); + + @override + Widget build(BuildContext context) { + return Obx(() { + return Visibility( + visible: MusicBar().isShow.value, + child: SafeArea( + top: false, + left: false, + right: false, + child: SizedBox( + height: musicBarHeight, + ), + ), + ); + }); + } +} diff --git a/lib/modules/splash/splash_binding.dart b/lib/modules/splash/splash_binding.dart deleted file mode 100644 index d89f924..0000000 --- a/lib/modules/splash/splash_binding.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:get/get.dart'; -import 'package:tone_snap/modules/splash/splash_controller.dart'; - -class SplashBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => SplashController()); - } -} diff --git a/lib/modules/splash/splash_controller.dart b/lib/modules/splash/splash_controller.dart deleted file mode 100644 index 3d84f21..0000000 --- a/lib/modules/splash/splash_controller.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.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/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 { - var processValue = 0.0.obs; - late AnimationController _controller; - late Animation _animation; - - @override - void onInit() { - super.onInit(); - _controller = AnimationController( - duration: const Duration(seconds: 1), - vsync: this, - ); - - // 创建 Tween 并绑定到 AnimationController - _animation = Tween(begin: 0, end: 1).animate(_controller) - ..addListener(() { - processValue.value = _animation.value; - if (processValue.value >= 1) { - // 显示开屏广告 - // AppOpenAdManager().showAdIfAvailable(onTap: _openInitial); - _openInitial(); - } - }); - - // 启动动画 - _controller.forward(); - - AppTrackingTransparencyManager().requestATT(); - } - - @override - void onReady() { - super.onReady(); - _getIp(); - } - - @override - void onClose() { - _controller.dispose(); - super.onClose(); - } - - /// 获取所在区域、ip - Future _getIp() async { - BaseModel? model = await TikUsTokApi.getIp(); - if (model != null && model.success && model.data?.isoCode != null) { - AppConfig.isoCode = model.data!.isoCode!; - } - } - - /// 打开首个页面 - void _openInitial() { - Get.offNamed(AppConfig.appSideEnum == AppSideEnum.sideA ? AppRoutes.initialA : AppRoutes.initialB); - } -} diff --git a/lib/res/themes/app_colors.dart b/lib/res/themes/app_colors.dart index da54dd4..6096032 100644 --- a/lib/res/themes/app_colors.dart +++ b/lib/res/themes/app_colors.dart @@ -6,4 +6,4 @@ import 'package:flutter/material.dart'; /// 种子颜色 const seedColor = Color(0xFF80F988); -const scaffoldBgColor = Color(0xFF151718); \ No newline at end of file +const scaffoldBgColor = Color(0xFF1A1A1A); \ No newline at end of file diff --git a/lib/res/themes/app_sizes.dart b/lib/res/themes/app_sizes.dart index 008549a..2cf6fe1 100644 --- a/lib/res/themes/app_sizes.dart +++ b/lib/res/themes/app_sizes.dart @@ -2,12 +2,7 @@ // 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 +double musicBarHeight = 73.5.h; \ No newline at end of file diff --git a/lib/res/themes/app_themes.dart b/lib/res/themes/app_themes.dart index 9130a5f..f2a5f64 100644 --- a/lib/res/themes/app_themes.dart +++ b/lib/res/themes/app_themes.dart @@ -3,7 +3,6 @@ // Description: 主题 import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:tone_snap/res/themes/app_colors.dart'; ThemeData sideATheme = ThemeData.dark(useMaterial3: true).copyWith( @@ -13,7 +12,7 @@ ThemeData sideATheme = ThemeData.dark(useMaterial3: true).copyWith( scrollbarTheme: ScrollbarThemeData( thumbVisibility: WidgetStateProperty.all(false), thumbColor: WidgetStateProperty.all(Colors.white), - radius: Radius.circular(8.r), + radius: const Radius.circular(8), ), ); diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index d0dbfea..03f1071 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -4,36 +4,38 @@ import 'package:flutter/animation.dart'; import 'package:get/get.dart'; +import 'package:tone_snap/modules/launch/launch_binding.dart'; +import 'package:tone_snap/modules/launch/launch_view.dart'; import 'package:tone_snap/modules/sidea/about/about_binding.dart'; import 'package:tone_snap/modules/sidea/about/about_view.dart'; import 'package:tone_snap/modules/sidea/change_voice/change_voice_binding.dart'; 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'; 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'; 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/album_song_list/album_song_list_binding.dart'; +import 'package:tone_snap/modules/sideb/album_song_list/album_song_list_view.dart'; +import 'package:tone_snap/modules/sideb/artists/artists_binding.dart'; +import 'package:tone_snap/modules/sideb/artists/artists_view.dart'; +import 'package:tone_snap/modules/sideb/custom_playlist/custom_playlist_binding.dart'; +import 'package:tone_snap/modules/sideb/custom_playlist/custom_playlist_view.dart'; 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/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/offline/offline_binding.dart'; +import 'package:tone_snap/modules/sideb/offline/offline_view.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/splash/splash_binding.dart'; -import 'package:tone_snap/modules/splash/splash_view.dart'; +import 'package:tone_snap/modules/sideb/search_result/search_result_binding.dart'; +import 'package:tone_snap/modules/sideb/search_result/search_result_view.dart'; +import 'package:tone_snap/modules/sideb/settings/settings_binding.dart'; +import 'package:tone_snap/modules/sideb/settings/settings_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'; @@ -43,21 +45,16 @@ class AppPages { static final routes = [ GetPage( - name: AppRoutes.splash, - page: () => SplashView(), - binding: SplashBinding(), + name: AppRoutes.launch, + page: () => LaunchView(), + binding: LaunchBinding(), ), /// SideA GetPage( name: AppRoutes.initialA, page: () => initial_view_a.InitialView(), - bindings: [ - initial_binding_a.InitialBinding(), - home_binding_a.HomeBinding(), - MeBinding(), - settings_binding_a.SettingsBinding(), - ], + binding: initial_binding_a.InitialBinding(), ), GetPage( name: AppRoutes.uploadMethod, @@ -102,32 +99,50 @@ class AppPages { GetPage( name: AppRoutes.initialB, page: () => initial_view_b.InitialView(), - bindings: [ - initial_binding_b.InitialBinding(), - home_binding_b.HomeBinding(), - SearchMusicBinding(), - PersonalMusicLibraryBinding(), - PlaylistsBinding(), - CollectPlaylistsBinding(), - ], + binding: initial_binding_b.InitialBinding(), ), GetPage( name: AppRoutes.playPage, page: () => PlayPageView(), binding: PlayPageBinding(), - transitionDuration: const Duration(milliseconds: 200), + // transitionDuration: const Duration(milliseconds: 200), transition: Transition.downToUp, curve: Curves.easeIn, ), GetPage( - name: AppRoutes.album, - page: () => AlbumView(), - binding: AlbumBinding(), + name: AppRoutes.albumSongList, + page: () => AlbumSongListView(), + binding: AlbumSongListBinding(), ), GetPage( name: AppRoutes.loveSongs, page: () => LoveSongsView(), binding: LoveSongsBinding(), ), + GetPage( + name: AppRoutes.artists, + page: () => ArtistsView(), + binding: ArtistsBinding(), + ), + GetPage( + name: AppRoutes.offline, + page: () => OfflineView(), + binding: OfflineBinding(), + ), + GetPage( + name: AppRoutes.customPlaylist, + page: () => CustomPlaylistView(), + binding: CustomPlaylistBinding(), + ), + GetPage( + name: AppRoutes.searchResult, + page: () => SearchResultView(), + binding: SearchResultBinding(), + ), + GetPage( + name: AppRoutes.setting, + page: () => SettingsView(), + binding: SettingsBinding(), + ), ]; } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 12d1f59..993b054 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -5,7 +5,7 @@ class AppRoutes { AppRoutes._(); - static const splash = '/splash'; + static const launch = '/launch'; /// SideA static const initialA = '/initialA'; @@ -20,6 +20,11 @@ class AppRoutes { /// SideB static const initialB = '/initialB'; static const playPage = '/play_page'; - static const album = '/album'; + static const albumSongList = '/album_song_list'; static const loveSongs = '/love_songs'; + static const artists = '/artists'; + static const offline = '/offline'; + static const customPlaylist = '/customPlaylist'; + static const searchResult = '/searchResult'; + static const setting = '/setting'; } diff --git a/lib/utils/audio_util.dart b/lib/utils/audio_util.dart index 5be4b4f..73abaa3 100644 --- a/lib/utils/audio_util.dart +++ b/lib/utils/audio_util.dart @@ -7,19 +7,24 @@ import 'package:audio_session/audio_session.dart'; class AudioUtil { static Future configAudioSession() async { final session = await AudioSession.instance; - await session.configure(AudioSessionConfiguration( - avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, - avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.allowBluetooth | AVAudioSessionCategoryOptions.defaultToSpeaker, - avAudioSessionMode: AVAudioSessionMode.spokenAudio, - avAudioSessionRouteSharingPolicy: AVAudioSessionRouteSharingPolicy.defaultPolicy, - avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none, - androidAudioAttributes: const AndroidAudioAttributes( - contentType: AndroidAudioContentType.speech, - flags: AndroidAudioFlags.none, - usage: AndroidAudioUsage.voiceCommunication, - ), - androidAudioFocusGainType: AndroidAudioFocusGainType.gain, - androidWillPauseWhenDucked: true, - )); + await session.configure(const AudioSessionConfiguration.music()); + // await session.configure(AudioSessionConfiguration( + // avAudioSessionCategory: AVAudioSessionCategory.playback, + // avAudioSessionCategoryOptions: + // AVAudioSessionCategoryOptions.allowBluetooth | + // AVAudioSessionCategoryOptions.allowAirPlay | + // AVAudioSessionCategoryOptions.defaultToSpeaker, + // avAudioSessionMode: AVAudioSessionMode.defaultMode, + // avAudioSessionRouteSharingPolicy: + // AVAudioSessionRouteSharingPolicy.defaultPolicy, + // avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none, + // androidAudioAttributes: const AndroidAudioAttributes( + // contentType: AndroidAudioContentType.music, + // flags: AndroidAudioFlags.none, + // usage: AndroidAudioUsage.media, + // ), + // androidAudioFocusGainType: AndroidAudioFocusGainType.gain, + // androidWillPauseWhenDucked: true, + // )); } -} \ No newline at end of file +} diff --git a/lib/utils/local_path_util.dart b/lib/utils/local_path_util.dart index 44fa169..a6d8c89 100644 --- a/lib/utils/local_path_util.dart +++ b/lib/utils/local_path_util.dart @@ -62,4 +62,9 @@ class LocalPathUtil { } return recordingsDir; } + + /// 获取音乐文件下载目录 + static String getMusicDownloadDir() { + return 'music_download'; + } } \ No newline at end of file diff --git a/lib/utils/log_util.dart b/lib/utils/log_util.dart index b77b465..ffcb007 100644 --- a/lib/utils/log_util.dart +++ b/lib/utils/log_util.dart @@ -16,8 +16,8 @@ final _logger = Logger( colors: true, // 是否打印表情符号 printEmojis: true, - // 是否打印时间 - printTime: false, + // 日期时间格式 + dateTimeFormat: DateTimeFormat.onlyTime, ), ); diff --git a/lib/utils/obj_util.dart b/lib/utils/obj_util.dart index d1a6fae..23cce01 100644 --- a/lib/utils/obj_util.dart +++ b/lib/utils/obj_util.dart @@ -3,12 +3,8 @@ // Description: 对象工具类 class ObjUtil { - static bool isNotEmptyStr(String? str) { - return str != null && str.trim().isNotEmpty; - } - static String getStr(String? str) { - return isNotEmptyStr(str) ? str! : ''; + return isNotEmpty(str) ? str! : ''; } static bool isNotEmptyList(Iterable? list) { @@ -19,6 +15,10 @@ class ObjUtil { return map != null && map.isNotEmpty; } + static bool isNotEmpty(Object? object) { + return !isEmpty(object); + } + static bool isEmpty(Object? object) { if (object == null) return true; if (object is String && object.trim().isEmpty) { @@ -31,10 +31,6 @@ class ObjUtil { return false; } - static bool isNotEmpty(Object? object) { - return !isEmpty(object); - } - /// Returns true Two List Is Equal. static bool twoListIsEqual(List? listA, List? listB) { if (listA == listB) return true; diff --git a/pubspec.yaml b/pubspec.yaml index 1794c02..fb48f04 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,20 +68,20 @@ dependencies: package_info_plus: ^8.0.0 # 日志打印 - logger: ^2.3.0 + logger: ^2.4.0 # webview webview_flutter: ^4.8.0 # 音频 - audio_session: ^0.1.19 + audio_session: ^0.1.21 flutter_sound: ^9.4.18 - just_audio: ^0.9.38 + just_audio: ^0.9.39 # ffmpeg_kit_flutter: ^6.0.3 ffmpeg_kit_flutter_audio: 6.0.3-LTS # 文件选择 - file_picker: ^8.0.5 + file_picker: ^8.0.6 # 跑马灯 widget_marquee: ^0.0.8 @@ -90,25 +90,31 @@ dependencies: app_tracking_transparency: ^2.0.5 # 网络 - dio: ^5.4.3+1 - pretty_dio_logger: ^1.3.1 - - # 获取设备当前语言 - devicelocale: ^0.7.1 + dio: ^5.5.0+1 + pretty_dio_logger: ^1.4.0 # 缓存管理器 - flutter_cache_manager: ^3.3.2 + flutter_cache_manager: ^3.3.3 # 广告 google_mobile_ads: ^5.1.0 # Firebase - firebase_core: ^3.1.1 - firebase_analytics: ^11.1.0 - firebase_crashlytics: ^4.0.2 + firebase_core: ^3.2.0 + firebase_analytics: ^11.2.0 + firebase_crashlytics: ^4.0.3 + firebase_remote_config: ^5.0.3 # 网络 - connectivity_plus: ^6.0.3 + connectivity_plus: ^6.0.4 + + # 后台文件下载,上传器 + background_downloader: ^8.5.2 + + # 对键盘可见性变化做出反应 + flutter_keyboard_visibility: ^6.0.0 + + step_progress_indicator: ^1.0.2 flutter_launcher_icons: android: "launcher_icon" diff --git a/test/widget_test.dart b/test/widget_test.dart index 1e5e2fc..4e9b1be 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -26,4 +26,4 @@ void main() { expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); -} +} \ No newline at end of file