From caded892d95a1ac9a9f29c83e969e9d83b61b276 Mon Sep 17 00:00:00 2001 From: fengshengxiong Date: Wed, 14 Aug 2024 13:56:43 +0800 Subject: [PATCH] =?UTF-8?q?1.=E6=96=B0=E5=A2=9E=E7=80=91=E5=B8=83=E6=B5=81?= =?UTF-8?q?=E6=8F=92=E9=A1=B5=E5=B9=BF=E5=91=8A=202.=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=A2=9E=E5=9F=8B=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle | 4 + android/app/google-services.json | 29 ++ android/app/src/main/AndroidManifest.xml | 2 +- android/settings.gradle | 4 + ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/Base.lproj/Main.storyboard | 13 +- lib/ads/app_open_ad_manager.dart | 19 +- lib/ads/interstitial_ad_manager.dart | 179 +++++++++-- lib/ads/library_native_ad_manager.dart | 94 ++++++ lib/data/api/music_api.dart | 4 + lib/data/enum/ad_format.dart | 15 + lib/data/enum/ad_scenes.dart | 39 +++ lib/data/models/ad_config_model.dart | 93 ++++++ lib/data/storage/music_box.dart | 48 ++- lib/data/storage/playlists_box.dart | 14 +- lib/firebase/firebase_analytics_manager.dart | 284 +++++++++++++++++- lib/firebase/firebase_options.dart | 18 +- .../firebase_remote_config_manager.dart | 6 + lib/global/app_config.dart | 264 ++++++++++++++++ lib/global/app_lifecycle_reactor.dart | 4 +- lib/global/download_manager.dart | 82 +++-- lib/global/network_connectivity_service.dart | 13 +- lib/main.dart | 16 +- lib/modules/launch/launch_controller.dart | 73 ++--- lib/modules/launch/launch_view.dart | 1 - .../change_voice/change_voice_controller.dart | 6 +- lib/modules/sidea/home/home_controller.dart | 32 +- .../sidea/initial/initial_controller.dart | 26 +- ...d_to_playlist_bottom_sheet_controller.dart | 3 +- .../album_song_list_controller.dart | 56 ++-- .../controllers/music_player_controller.dart | 28 ++ .../custom_playlist_controller.dart | 46 ++- lib/modules/sideb/home/home_controller.dart | 60 +++- lib/modules/sideb/home/home_view.dart | 2 +- .../sideb/initial/initial_controller.dart | 18 +- .../love_songs/love_songs_controller.dart | 17 +- .../more_bottom_sheet_controller.dart | 11 + .../sideb/music_bar/music_bar_controller.dart | 13 +- .../sideb/offline/offline_controller.dart | 17 +- .../personal_music_library_controller.dart | 37 ++- .../personal_music_library_view.dart | 24 +- .../sideb/play_page/play_page_controller.dart | 38 +++ .../sideb/play_page/play_page_view.dart | 4 +- .../sideb/playlists/playlists_controller.dart | 4 +- .../search_music/search_music_controller.dart | 2 + .../search_result_controller.dart | 34 ++- .../search_result_child_controller.dart | 15 +- ...earch_result_child_optimum_controller.dart | 15 +- pubspec.yaml | 4 +- 49 files changed, 1551 insertions(+), 285 deletions(-) create mode 100644 android/app/google-services.json create mode 100644 lib/ads/library_native_ad_manager.dart create mode 100644 lib/data/enum/ad_format.dart create mode 100644 lib/data/enum/ad_scenes.dart create mode 100644 lib/data/models/ad_config_model.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index a1a328e..d0f0f9c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,5 +1,9 @@ plugins { id "com.android.application" + // START: FlutterFire Configuration + id 'com.google.gms.google-services' + id 'com.google.firebase.crashlytics' + // END: FlutterFire Configuration id "kotlin-android" // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..7763b09 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "280378307504", + "project_id": "tonesnap-and", + "storage_bucket": "tonesnap-and.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:280378307504:android:bcee5d63088dcbb7ca5c75", + "android_client_info": { + "package_name": "com.tone.music.offline" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCLEOFjenMfiZKdoKdGh9mylckpIpTn854" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 34cc3db..9160878 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -48,7 +48,7 @@ + android:value="ca-app-pub-5684307632319406~5753747604"/> @@ -14,13 +16,14 @@ - + - + + diff --git a/lib/ads/app_open_ad_manager.dart b/lib/ads/app_open_ad_manager.dart index 7b480a8..12ee7d5 100644 --- a/lib/ads/app_open_ad_manager.dart +++ b/lib/ads/app_open_ad_manager.dart @@ -20,17 +20,17 @@ class AppOpenAdManager { factory AppOpenAdManager() => _instance; + AppOpenAd? _appOpenAd; + bool _isShowingAd = false; + /// 加载和显示广告之间允许的最长持续时间 - final Duration maxCacheDuration = const Duration(hours: 4); + final Duration _maxCacheDuration = const Duration(minutes: 50); /// 跟踪加载时间,这样我们就不会显示过期的广告 DateTime? _appOpenLoadTime; - AppOpenAd? _appOpenAd; - bool _isShowingAd = false; - /// 记录关闭时的时间,用于下一次展示时计算时间差 - DateTime? closeDateTime; + DateTime? _closeDateTime; /// 开屏广告单元id final adUnitId = Platform.isAndroid @@ -41,6 +41,7 @@ class AppOpenAdManager { Future loadAd() async { final List connectivityResult = await (Connectivity().checkConnectivity()); if (connectivityResult.contains(ConnectivityResult.none)) { + LogUtil.d('当前无网络,不加载广告'); return; } if (isAdAvailable) { @@ -87,7 +88,7 @@ class AppOpenAdManager { if(onTap != null) onTap(); return; } - if (DateTime.now().subtract(maxCacheDuration).isAfter(_appOpenLoadTime!)) { + if (DateTime.now().subtract(_maxCacheDuration).isAfter(_appOpenLoadTime!)) { LogUtil.d('超过了最大缓存持续时间。正在加载另一个广告'); _appOpenAd!.dispose(); _appOpenAd = null; @@ -95,9 +96,9 @@ class AppOpenAdManager { if(onTap != null) onTap(); return; } - if (closeDateTime != null) { + if (_closeDateTime != null) { // 计算时间差 - Duration timeDifference = DateTime.now().difference(closeDateTime!); + Duration timeDifference = DateTime.now().difference(_closeDateTime!); // 获取配置的 openAppEventDuration int openAppEventDuration = MusicBox().getOpenAppEventDuration(); // 检查时间差是否小于10秒 @@ -130,7 +131,7 @@ class AppOpenAdManager { }, onAdDismissedFullScreenContent: (ad) { LogUtil.d('$ad onAdDismissedFullScreenContent'); - closeDateTime = DateTime.now(); + _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 7a44ec1..2e44a39 100644 --- a/lib/ads/interstitial_ad_manager.dart +++ b/lib/ads/interstitial_ad_manager.dart @@ -2,12 +2,16 @@ // Date: 2024/6/25 // Description: 插页广告 -import 'dart:io' show Platform; - import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; +import 'package:tone_snap/data/enum/ad_scenes.dart'; +import 'package:tone_snap/data/models/ad_config_model.dart'; +import 'package:tone_snap/data/storage/music_box.dart'; +import 'package:tone_snap/firebase/firebase_analytics_manager.dart'; +import 'package:tone_snap/modules/launch/launch_controller.dart'; import 'package:tone_snap/utils/log_util.dart'; +import 'package:tone_snap/utils/obj_util.dart'; class InterstitialAdManager { InterstitialAdManager._(); @@ -16,97 +20,208 @@ class InterstitialAdManager { factory InterstitialAdManager() => _instance; + /// 广告对象Map + final Map _interstitialAdMap = {}; + /// 加载和显示广告之间允许的最长持续时间 - final Duration maxCacheDuration = const Duration(hours: 4); + final Duration _maxCacheDuration = const Duration(minutes: 50); /// 跟踪加载时间,这样我们就不会显示过期的广告 - DateTime? _appOpenLoadTime; + final Map _appOpenLoadTimeMap = {}; - InterstitialAd? _interstitialAd; + /// 记录关闭时的时间,用于下一次展示时计算时间差 + DateTime? _closeDateTime; + + /// 是否显示 bool isShowingAd = false; - /// 插页广告单元id - final adUnitId = Platform.isAndroid - ? (kReleaseMode ? '' : 'ca-app-pub-3940256099942544/1033173712') - : (kReleaseMode ? 'ca-app-pub-5684307632319406/2760767691' : 'ca-app-pub-3940256099942544/4411468910'); + /// 加载所有插页广告位 + void loadAdAll() { + AdConfigModel adConfigModel = MusicBox().getAdConfig(); + _loadSingleAd(adConfigModel.coldLoading, AdScenes.coldLoading.name); + _loadSingleAd(adConfigModel.hotLoading, AdScenes.hotLoading.name); + _loadSingleAd(adConfigModel.play, AdScenes.play.name); + _loadSingleAd(adConfigModel.download, AdScenes.download.name); + _loadSingleAd(adConfigModel.list, AdScenes.list.name); + _loadSingleAd(adConfigModel.playCut, AdScenes.playCut.name); + _loadSingleAd(adConfigModel.search, AdScenes.search.name); + } + + /// 加载单个广告位 + void _loadSingleAd(List? adList, String adScenes) { + if (adList != null && adList.isNotEmpty) { + var adModel = adList.reduce((value, element) => element.level! < value.level! ? element : value); + loadAd(adModel.identifier, adScenes); + } + } /// 加载广告 - void loadAd() async { + void loadAd(String? adUnitId, String adScenes) async { final List connectivityResult = await (Connectivity().checkConnectivity()); if (connectivityResult.contains(ConnectivityResult.none)) { + LogUtil.d('当前无网络,不加载广告'); return; } - if (isAdAvailable) { + if (ObjUtil.isEmpty(adUnitId)) { return; } InterstitialAd.load( - adUnitId: adUnitId, + adUnitId: adUnitId!, request: const AdRequest(), adLoadCallback: InterstitialAdLoadCallback( onAdLoaded: (ad) { - LogUtil.d('插页广告加载完成'); - _interstitialAd = ad; - _appOpenLoadTime = DateTime.now(); + LogUtil.d('$adScenes:${ad.adUnitId}插页广告加载完成'); + _interstitialAdMap[adScenes] = ad; + _appOpenLoadTimeMap[adScenes] = DateTime.now(); + if (adScenes == AdScenes.coldLoading.name) { + if (Get.isRegistered()) { + LaunchController.to.editChangeValue(); + } + } }, onAdFailedToLoad: (LoadAdError error) { - LogUtil.d('插页广告加载失败: $error'); + LogUtil.d('$adScenes:$adUnitId插页广告加载失败: $error'); + _loadFailHandle(adUnitId, adScenes); + FirebaseAnalyticsManager.logPlayAdsLoadFailure(error.message); }, ), ); } + /// 加载失败处理 + void _loadFailHandle(String adUnitId, String adScenes) { + AdConfigModel adConfigModel = MusicBox().getAdConfig(); + var adList = _getAdList(adScenes); + if (adList != null) { + var currentItem = adList.firstWhereOrNull((e) => e.identifier == adUnitId); + if (currentItem != null) { + var nextItem = adList.firstWhereOrNull((e) => e.level == currentItem.level! + 1); + if (nextItem != null) { + // 当前id加载失败,加载下一个id + loadAd(nextItem.identifier, adScenes); + } else { + // 对应场景id都加载失败,加载全局id + _loadSingleAd(adConfigModel.backup, adScenes); + } + } else { + var currentItem = adConfigModel.backup?.firstWhereOrNull((e) => e.identifier == adUnitId); + if (currentItem != null) { + var nextItem = adConfigModel.backup?.firstWhereOrNull((e) => e.level == currentItem.level! + 1); + if (nextItem != null) { + loadAd(nextItem.identifier, adScenes); + } else { + if (adList.isNotEmpty) { + _loadSingleAd(adList, adScenes); + } + } + } + } + } + } + + List? _getAdList(String adScenes) { + AdConfigModel adConfigModel = MusicBox().getAdConfig(); + if (adScenes == AdScenes.coldLoading.name) { + return adConfigModel.coldLoading; + } else if (adScenes == AdScenes.hotLoading.name) { + return adConfigModel.hotLoading; + } else if (adScenes == AdScenes.play.name) { + return adConfigModel.play; + } else if (adScenes == AdScenes.download.name) { + return adConfigModel.download; + } else if (adScenes == AdScenes.list.name) { + return adConfigModel.list; + } else if (adScenes == AdScenes.playCut.name) { + return adConfigModel.playCut; + } else if (adScenes == AdScenes.search.name) { + return adConfigModel.search; + } else if (adScenes == AdScenes.backup.name) { + return adConfigModel.backup; + } + return null; + } + /// 是否可以播放广告 - bool get isAdAvailable { - return _interstitialAd != null; + bool isAdAvailable(String adScenes) { + return _interstitialAdMap[adScenes] != null; } /// 显示广告(如果存在且尚未显示) /// 如果先前缓存的广告已过期,则只加载并缓存新广告 - void showAdIfAvailable({Function()? onTap}) { - if (!isAdAvailable) { + void showAdIfAvailable(String adScenes, {Function()? onTap}) { + if (!isAdAvailable(adScenes)) { LogUtil.d('尝试在可用之前显示广告'); - loadAd(); + if(onTap != null) onTap(); return; } if (isShowingAd) { LogUtil.d('尝试在已显示广告的情况下显示广告'); + if(onTap != null) onTap(); return; } - if (DateTime.now().subtract(maxCacheDuration).isAfter(_appOpenLoadTime!)) { + if (DateTime.now().subtract(_maxCacheDuration).isAfter(_appOpenLoadTimeMap[adScenes]!)) { LogUtil.d('超过了最大缓存持续时间。正在加载另一个广告'); - _interstitialAd!.dispose(); - _interstitialAd = null; - loadAd(); + _interstitialAdMap[adScenes]!.dispose(); + loadAd(_interstitialAdMap[adScenes]?.adUnitId, adScenes); + _interstitialAdMap[adScenes] = null; + _interstitialAdMap.remove(adScenes); + if(onTap != null) onTap(); return; } + if (_closeDateTime != null) { + // 计算时间差 + Duration timeDifference = DateTime.now().difference(_closeDateTime!); + + int seconds; + if (adScenes == AdScenes.hotLoading.name) { + seconds = MusicBox().getOpenAppEventDuration(); + } else { + seconds = MusicBox().getInterstitialEventDuration(); + } + if (timeDifference < Duration(seconds: seconds)) { + LogUtil.d('距离上次广告展示未达到插页广告事件间隔时长'); + + if(onTap != null) onTap(); + return; + } + } + + FirebaseAnalyticsManager.logPlayAdsChanceAction(); + // 设置 fullScreenContentCallback 并显示广告 - _interstitialAd!.fullScreenContentCallback = FullScreenContentCallback( + _interstitialAdMap[adScenes]!.fullScreenContentCallback = FullScreenContentCallback( // 暂停应用程序中的活动或记录广告展示的时间 onAdShowedFullScreenContent: (ad) { - isShowingAd = true; LogUtil.d('$ad onAdShowedFullScreenContent'); + isShowingAd = true; }, // 更适合用于跟踪广告展示的次数和效果,以及执行与广告展示相关的操作 onAdImpression: (ad) { LogUtil.d('$ad onAdImpression'); + FirebaseAnalyticsManager.logPlayAdsShowSuccess(ad); }, onAdFailedToShowFullScreenContent: (ad, error) { LogUtil.d('$ad onAdFailedToShowFullScreenContent: $error'); isShowingAd = false; ad.dispose(); - _interstitialAd = null; + loadAd(_interstitialAdMap[adScenes]?.adUnitId, adScenes); + _interstitialAdMap[adScenes] = null; + _interstitialAdMap.remove(adScenes); + FirebaseAnalyticsManager.logPlayAdsLoadFailure(error.message); if(onTap != null) onTap(); }, onAdDismissedFullScreenContent: (ad) { LogUtil.d('$ad onAdDismissedFullScreenContent'); isShowingAd = false; + _closeDateTime = DateTime.now(); ad.dispose(); - _interstitialAd = null; - loadAd(); + loadAd(_interstitialAdMap[adScenes]?.adUnitId, adScenes); + _interstitialAdMap[adScenes] = null; + _interstitialAdMap.remove(adScenes); if(onTap != null) onTap(); }, @@ -114,6 +229,6 @@ class InterstitialAdManager { LogUtil.d('$ad onAdClicked'); }, ); - _interstitialAd!.show(); + _interstitialAdMap[adScenes]!.show(); } } \ No newline at end of file diff --git a/lib/ads/library_native_ad_manager.dart b/lib/ads/library_native_ad_manager.dart new file mode 100644 index 0000000..a7fc693 --- /dev/null +++ b/lib/ads/library_native_ad_manager.dart @@ -0,0 +1,94 @@ +// Author: fengshengxiong +// Date: 2024/6/25 +// Description: 原生广告 + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:google_mobile_ads/google_mobile_ads.dart'; +import 'package:tone_snap/data/enum/ad_scenes.dart'; +import 'package:tone_snap/data/models/ad_config_model.dart'; +import 'package:tone_snap/data/storage/music_box.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; +import 'package:tone_snap/utils/log_util.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class LibraryNativeAdManager { + LibraryNativeAdManager._(); + + static final LibraryNativeAdManager _instance = LibraryNativeAdManager._(); + + factory LibraryNativeAdManager() => _instance; + + /// 广告对象 + NativeAd? nativeAd; + + /// 广告是否已加载 + var nativeAdIsLoaded = false.obs; + + /// 加载广告 + void loadNativeAd() { + AdConfigModel adConfigModel = MusicBox().getAdConfig(); + if (adConfigModel.library != null && adConfigModel.library!.isNotEmpty) { + _loadAd(adConfigModel.library![0].identifier, AdScenes.library.name); + } + } + + /// 加载广告 + void _loadAd(String? adUnitId, String adScenes) async { + if (ObjUtil.isEmpty(adUnitId)) { + return; + } + nativeAd = NativeAd( + adUnitId: adUnitId!, + request: const AdRequest(), + listener: NativeAdListener( + onAdLoaded: (ad) { + LogUtil.d('$adScenes:${ad.adUnitId}原生广告加载完成'); + nativeAdIsLoaded.value = true; + }, + onAdFailedToLoad: (ad, error) { + LogUtil.e('$adScenes:${ad.adUnitId}原生广告加载失败: $error'); + nativeAdIsLoaded.value = false; + loadNativeAd(); + ad.dispose(); + }, + onAdClicked: (ad) {}, + onAdImpression: (ad) { + + }, + onAdClosed: (ad) {}, + onAdOpened: (ad) {}, + onAdWillDismissScreen: (ad) {}, + onPaidEvent: (ad, valueMicros, precision, currencyCode) { + + }, + ), + nativeTemplateStyle: NativeTemplateStyle( + templateType: TemplateType.medium, + mainBackgroundColor: scaffoldBgColor, + cornerRadius: 8.0, + callToActionTextStyle: NativeTemplateTextStyle( + backgroundColor: seedColor, + textColor: Colors.white, + style: NativeTemplateFontStyle.bold, + size: 16.0, + ), + primaryTextStyle: NativeTemplateTextStyle( + textColor: seedColor, + style: NativeTemplateFontStyle.bold, + size: 14.0, + ), + secondaryTextStyle: NativeTemplateTextStyle( + textColor: Colors.white, + style: NativeTemplateFontStyle.italic, + size: 12.0, + ), + tertiaryTextStyle: NativeTemplateTextStyle( + textColor: seedColor, + style: NativeTemplateFontStyle.normal, + size: 14.0, + ), + ), + )..load(); + } +} \ No newline at end of file diff --git a/lib/data/api/music_api.dart b/lib/data/api/music_api.dart index 34c9be4..2d57c51 100644 --- a/lib/data/api/music_api.dart +++ b/lib/data/api/music_api.dart @@ -20,6 +20,7 @@ class MusicApi { String? visitorData, Map? queryParameters, T Function(Map)? formJson, + Function(BaseError baseError)? fail, bool showLoading = false, bool showToast = false, }) async { @@ -47,6 +48,7 @@ class MusicApi { queryParameters: queryParameters, formJson: formJson, success: (data) => model = data, + fail: fail ); return model; } @@ -173,6 +175,7 @@ class MusicApi { static Future search({ Map? queryParameters, T Function(Map)? formJson, + Function(BaseError baseError)? fail, bool showLoading = false, bool showToast = true, }) async { @@ -199,6 +202,7 @@ class MusicApi { queryParameters: queryParameters, formJson: formJson, success: (data) => model = data, + fail: fail, ); return model; } diff --git a/lib/data/enum/ad_format.dart b/lib/data/enum/ad_format.dart new file mode 100644 index 0000000..bc44a6f --- /dev/null +++ b/lib/data/enum/ad_format.dart @@ -0,0 +1,15 @@ +// Author: fengshengxiong +// Date: 2024/6/13 +// Description: 广告格式 + +enum AdFormat { + /// 插页 + insert('Insert'), + + /// 原生 + native('Native'); + + const AdFormat(this.type); + + final String type; +} diff --git a/lib/data/enum/ad_scenes.dart b/lib/data/enum/ad_scenes.dart new file mode 100644 index 0000000..365df22 --- /dev/null +++ b/lib/data/enum/ad_scenes.dart @@ -0,0 +1,39 @@ +// Author: fengshengxiong +// Date: 2024/6/13 +// Description: 广告场景 + +enum AdScenes { + /// 冷启动cold_loading(插页式广告) + coldLoading('coldLoading'), + + /// 热启动hot_loading(插页式广告) + hotLoading('hotLoading'), + + /// 播放play(插页式广告) + play('play'), + + /// 下载download(插页式广告) + download('download'), + + /// 列表list(插页式广告) + list('list'), + + /// 曲库library(原生高级广告) + library('library'), + + /// 切歌play_cut(插页式广告) + playCut('playCut'), + + /// 点击搜索search(插页式广告) + search('search'), + + /// 搜索结果展示search(原生高级广告) + searchResult('searchResult'), + + /// 全局backup(插页式广告) + backup('backup'); + + const AdScenes(this.name); + + final String name; +} diff --git a/lib/data/models/ad_config_model.dart b/lib/data/models/ad_config_model.dart new file mode 100644 index 0000000..cedb959 --- /dev/null +++ b/lib/data/models/ad_config_model.dart @@ -0,0 +1,93 @@ +// Author: fengshengxiong +// Date: 2024/8/6 +// Description: 广告配置模型 + +import 'dart:convert'; + +class AdConfigModel { + List? coldLoading; + List? hotLoading; + List? play; + List? download; + List? list; + List? library; + List? playCut; + List? search; + List? searchResult; + List? backup; + + AdConfigModel({ + this.coldLoading, + this.hotLoading, + this.play, + this.download, + this.list, + this.library, + this.playCut, + this.search, + this.searchResult, + this.backup, + }); + + factory AdConfigModel.fromJson(String str) => AdConfigModel.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory AdConfigModel.fromMap(Map json) => AdConfigModel( + coldLoading: json["coldLoading"] == null ? [] : List.from(json["coldLoading"]!.map((x) => AdModel.fromMap(x))), + hotLoading: json["hotLoading"] == null ? [] : List.from(json["hotLoading"]!.map((x) => AdModel.fromMap(x))), + play: json["play"] == null ? [] : List.from(json["play"]!.map((x) => AdModel.fromMap(x))), + download: json["download"] == null ? [] : List.from(json["download"]!.map((x) => AdModel.fromMap(x))), + list: json["list"] == null ? [] : List.from(json["list"]!.map((x) => AdModel.fromMap(x))), + library: json["library"] == null ? [] : List.from(json["library"]!.map((x) => AdModel.fromMap(x))), + playCut: json["playCut"] == null ? [] : List.from(json["playCut"]!.map((x) => AdModel.fromMap(x))), + search: json["search"] == null ? [] : List.from(json["search"]!.map((x) => AdModel.fromMap(x))), + searchResult: json["searchResult"] == null ? [] : List.from(json["searchResult"]!.map((x) => AdModel.fromMap(x))), + backup: json["backup"] == null ? [] : List.from(json["backup"]!.map((x) => AdModel.fromMap(x))), + ); + + Map toMap() => { + "coldLoading": coldLoading == null ? [] : List.from(coldLoading!.map((x) => x.toMap())), + "hotLoading": hotLoading == null ? [] : List.from(hotLoading!.map((x) => x.toMap())), + "play": play == null ? [] : List.from(play!.map((x) => x.toMap())), + "download": download == null ? [] : List.from(download!.map((x) => x.toMap())), + "list": list == null ? [] : List.from(list!.map((x) => x.toMap())), + "library": library == null ? [] : List.from(library!.map((x) => x.toMap())), + "playCut": playCut == null ? [] : List.from(playCut!.map((x) => x.toMap())), + "search": search == null ? [] : List.from(search!.map((x) => x.toMap())), + "searchResult": searchResult == null ? [] : List.from(searchResult!.map((x) => x.toMap())), + "backup": backup == null ? [] : List.from(backup!.map((x) => x.toMap())), + }; +} + +class AdModel { + int? level; + String? identifier; + String? ad; + String? type; + + AdModel({ + this.level, + this.identifier, + this.ad, + this.type, + }); + + factory AdModel.fromJson(String str) => AdModel.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory AdModel.fromMap(Map json) => AdModel( + level: json["level"], + identifier: json["identifier"], + ad: json["ad"], + type: json["type"], + ); + + Map toMap() => { + "level": level, + "identifier": identifier, + "ad": ad, + "type": type, + }; +} diff --git a/lib/data/storage/music_box.dart b/lib/data/storage/music_box.dart index 9d89d21..eb5999d 100644 --- a/lib/data/storage/music_box.dart +++ b/lib/data/storage/music_box.dart @@ -3,10 +3,12 @@ // Description: 音乐非结构化数据盒子 import 'dart:convert'; +import 'dart:io'; 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/models/ad_config_model.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'; @@ -21,21 +23,24 @@ class MusicBox { return _instance; } - /// RemoteConfig openStatus - final _openStatusKey = 'openStatusKey'; - - /// RemoteConfig dataVersion - final _dataVersionKey = 'dataVersionKey'; - /// 是否进入过B面 final _isOpenedSideBKey = 'isOpenedSideBKey'; + /// openStatus + final _openStatusKey = 'openStatusKey'; + + /// dataVersion + final _dataVersionKey = 'dataVersionKey'; + /// 开屏启动插页事件间隔时长 final _openAppEventDurationKey = 'openAppEventDurationKey'; /// 插页广告事件间隔时长 final _interstitialEventDurationKey = 'interstitialEventDurationKey'; + /// 广告配置 + final _adMobLevelIDsKey = 'adMobLevelIDsKey'; + /// 播放模式 final _playModeKey = 'playModeKey'; @@ -46,6 +51,16 @@ class MusicBox { /// 注意, main函数中这个盒子已经打开, 可以进行存储操作 final _box = Hive.box(musicBox); + /// 设置是否打开过B面 + Future putIsOpenedSideB(bool isOpen) async { + return await _box.put(_isOpenedSideBKey, isOpen); + } + + /// 获取是否打开过B面 + bool getIsOpenedSideB() { + return _box.get(_isOpenedSideBKey, defaultValue: false); + } + /// 设置 openStatus Future putOpenStatus(String openStatus) async { return await _box.put(_openStatusKey, openStatus); @@ -114,16 +129,6 @@ class MusicBox { 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); @@ -144,6 +149,17 @@ class MusicBox { return _box.get(_interstitialEventDurationKey, defaultValue: AppConfig.interstitialEventDuration); } + /// 设置广告配置 + Future putAdConfig(String adConfigStr) async { + return await _box.put(_adMobLevelIDsKey, adConfigStr); + } + + /// 获取广告配置 + AdConfigModel getAdConfig() { + var configStr = _box.get(_adMobLevelIDsKey, defaultValue: Platform.isIOS ? AppConfig.adIOSDefaultConfig : AppConfig.adAndroidDefaultConfig); + return AdConfigModel.fromJson(configStr); + } + /// 设置播放模式 Future putPlayMode(PlayMode playMode) async { return await _box.put(_playModeKey, playMode); diff --git a/lib/data/storage/playlists_box.dart b/lib/data/storage/playlists_box.dart index 3a682b5..b8e75a6 100644 --- a/lib/data/storage/playlists_box.dart +++ b/lib/data/storage/playlists_box.dart @@ -9,6 +9,7 @@ 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'; +import 'package:tone_snap/utils/log_util.dart'; class PlaylistsBox { PlaylistsBox._(); @@ -25,12 +26,13 @@ class PlaylistsBox { /// 获取列表 List getList() { - return _box.values.toList(); - } - - /// 获取倒序列表 - List getReversedList() { - return _box.values.toList().reversed.toList(); + var list = _box.values.toList(); + try { + list.sort((a, b) => b.milliseconds!.compareTo(a.milliseconds!)); + } catch (e) { + LogUtil.e(e.toString()); + } + return list; } /// 添加数据 diff --git a/lib/firebase/firebase_analytics_manager.dart b/lib/firebase/firebase_analytics_manager.dart index 36acf0a..ab66268 100644 --- a/lib/firebase/firebase_analytics_manager.dart +++ b/lib/firebase/firebase_analytics_manager.dart @@ -3,17 +3,291 @@ // Description: firebase_analytics管理 import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:google_mobile_ads/google_mobile_ads.dart'; +import 'package:tone_snap/utils/log_util.dart'; +import 'package:tone_snap/utils/obj_util.dart'; class FirebaseAnalyticsManager { - static const homeApv = 'home_a_pv'; - /// 埋点 /// name:事件名 /// parameters:业务参数 static void logEvent(String eventName, {Map? parameters}) { - FirebaseAnalytics.instance.logEvent( - name: eventName, - parameters: parameters, + try { + FirebaseAnalytics.instance.logEvent( + name: eventName, + parameters: parameters, + ); + } catch (e) { + LogUtil.e(e.toString()); + } + } + + /// 启动页曝光 + static void logLaunchPV() { + logEvent('launch_pv'); + } + + /// 跳转 + static void logJumpEvent(String? side, String? reason) { + logEvent('jump_event', + parameters: { + 'side': ObjUtil.getStr(side), + 'reason': ObjUtil.getStr(reason) + }, + ); + } + + /// A面首页曝光 + static void logHomeAPV() { + logEvent('home_a_pv'); + } + + /// B面首页曝光 + static void logHomeBPV() { + logEvent('home_b_pv'); + } + + /// 首页资源曝光成功 + static void logHomeBModuleShowsuccesAction() { + logEvent('home_b_module_showsucces_action'); + } + + /// 首页资源曝光失败 + static void logHomeBModuleFailAction(String? failreason) { + logEvent('home_b_module_fail_action', + parameters: { + 'failreason': ObjUtil.getStr(failreason) + }, + ); + } + + /// 点击首页模块 + static void logHomeBModuleClick(String? modulename) { + logEvent('home_b_module_click', + parameters: { + 'modulename': ObjUtil.getStr(modulename) + }, + ); + } + + /// B面我的曝光 + static void logMeBPV() { + logEvent('me_b_pv'); + } + + /// 点击歌曲 + static void logSongClick(String? songfrom) { + logEvent('song_click', + parameters: { + 'songfrom': ObjUtil.getStr(songfrom) + }, + ); + } + + /// B面歌曲曝光 + static void logPlayerBpv(String? videoid, String? videoname, String? artistname) { + logEvent('player_b_pv', + parameters: { + 'videoid': ObjUtil.getStr(videoid), + 'videoname': ObjUtil.getStr(videoname), + 'artistname': ObjUtil.getStr(artistname), + }, + ); + } + + /// 统计加载时间 + static void logPlayerBDelayAction(String? videoid, String? videoname, String? artistname, String? delay) { + logEvent('player_b_delay_action', + parameters: { + 'videoid': ObjUtil.getStr(videoid), + 'videoname': ObjUtil.getStr(videoname), + 'artistname': ObjUtil.getStr(artistname), + 'delay': ObjUtil.getStr(delay), + }, + ); + } + + /// 播放成功 + static void logPlayerBSuccessAction(String? videoid, String? videoname, String? artistname) { + logEvent('player_b_success_action', + parameters: { + 'videoid': ObjUtil.getStr(videoid), + 'videoname': ObjUtil.getStr(videoname), + 'artistname': ObjUtil.getStr(artistname), + }, + ); + } + + /// 播放失败 + static void logPlayerBFailAction(String? failreason) { + logEvent('player_b_fail_action', + parameters: { + 'failreason': ObjUtil.getStr(failreason) + }, + ); + } + + /// 收藏 + static void logPlayerBLoveClick(String? videoid, String? videoname, String? artistname) { + logEvent('player_b_love_click', + parameters: { + 'videoid': ObjUtil.getStr(videoid), + 'videoname': ObjUtil.getStr(videoname), + 'artistname': ObjUtil.getStr(artistname), + }, + ); + } + + /// 取消收藏 + static void logPlayerBUnloveClick(String? videoid, String? videoname, String? artistname) { + logEvent('player_b_unlove_click', + parameters: { + 'videoid': ObjUtil.getStr(videoid), + 'videoname': ObjUtil.getStr(videoname), + 'artistname': ObjUtil.getStr(artistname), + }, + ); + } + + /// 点击下载 + static void logPlayerBDownloadClick(String? videoid, String? videoname, String? artistname) { + logEvent('player_b_download_click', + parameters: { + 'videoid': ObjUtil.getStr(videoid), + 'videoname': ObjUtil.getStr(videoname), + 'artistname': ObjUtil.getStr(artistname), + }, + ); + } + + /// 下载成功 + static void logPlayerBDownloadsuccessAction(String? videoid, String? videoname, String? artistname) { + logEvent('player_b_downloadsuccess_action', + parameters: { + 'videoid': ObjUtil.getStr(videoid), + 'videoname': ObjUtil.getStr(videoname), + 'artistname': ObjUtil.getStr(artistname), + }, + ); + } + + /// 下载失败 + static void logPlayerBDownloadfailAction(String? failreason) { + logEvent('player_b_downloadfail_action', + parameters: { + 'failreason': ObjUtil.getStr(failreason) + }, + ); + } + + /// B面搜索曝光 + static void logSearchPV() { + logEvent('search_pv'); + } + + /// 搜索SUG曝光 + static void logSearchSugShow() { + logEvent('search_sug_show'); + } + + /// 输入搜索内容,点击搜索(包含联想点击) + static void logSearchSugClick(String? sugname) { + logEvent('search_sug_click', + parameters: { + 'sugname': ObjUtil.getStr(sugname) + }, + ); + } + + /// 搜索结果曝光 + static void logSearchResultPV() { + logEvent('search_result_pv'); + } + + /// 搜索有结果 + static void logSearchResultsuccessAction() { + logEvent('search_resultsuccess_action'); + } + + /// 搜索无结果 + static void logSearchResultfailAction(String? failreason) { + logEvent('search_resultfail_action', + parameters: { + 'failreason': ObjUtil.getStr(failreason) + }, + ); + } + + /// 搜索点击 + static void logSearchFromAction(String? searchfrom) { + logEvent('search_from_action', + parameters: { + 'searchfrom': ObjUtil.getStr(searchfrom) + }, + ); + } + + /// 播放器弹出 + static void logPlayerBImp() { + logEvent('player_b_imp'); + } + + /// 列表加载 + static void logPlayerBList() { + logEvent('player_b_list'); + } + + /// 创建歌单 + static void logCreateList() { + logEvent('create_list'); + } + + /// 曲库点击 + static void logLibraryClick(String? clickfrom, String? folderclick) { + logEvent('library_click', + parameters: { + 'clickfrom': ObjUtil.getStr(clickfrom), + 'folderclick': ObjUtil.getStr(folderclick) + }, + ); + } + + /// 广告展示机会 + static void logPlayAdsChanceAction() { + logEvent('play_ads_chance'); + } + + /// 广告加载失败 + static void logPlayAdsLoadFailure(String? error) { + logEvent('play_ads_loadFailure', + parameters: { + 'error': ObjUtil.getStr(error), + }, + ); + } + + /// 广告展示成功 + static void logPlayAdsShowSuccess(Ad ad) { + var mediation = ''; + if (ad.responseInfo != null && ad.responseInfo!.responseExtras.isNotEmpty) { + mediation = ad.responseInfo!.responseExtras['mediation_group_name']; + } + logEvent('play_ads_showSuccess', + parameters: { + 'platform': ObjUtil.getStr(ad.responseInfo?.loadedAdapterResponseInfo?.adSourceName), + 'source': ObjUtil.getStr(ad.responseInfo?.loadedAdapterResponseInfo?.adSourceInstanceName), + 'unitName': ObjUtil.getStr(mediation), + }, + ); + + } + + /// 广告展示失败 + static void logPlayAdsShowFailure(String? error) { + logEvent('play_ads_showFailure', + parameters: { + 'error': ObjUtil.getStr(error), + }, ); } } diff --git a/lib/firebase/firebase_options.dart b/lib/firebase/firebase_options.dart index ec6df03..f20d44c 100644 --- a/lib/firebase/firebase_options.dart +++ b/lib/firebase/firebase_options.dart @@ -23,8 +23,8 @@ class DefaultFirebaseOptions { ); } switch (defaultTargetPlatform) { - // case TargetPlatform.android: - // return android; + case TargetPlatform.android: + return android; case TargetPlatform.iOS: return ios; case TargetPlatform.macOS: @@ -49,13 +49,13 @@ class DefaultFirebaseOptions { } } - // static const FirebaseOptions android = FirebaseOptions( - // apiKey: 'AIzaSyDP6CpkN3HMpCofXlToug-hadYpTLEgE0E', - // appId: '1:318284530945:android:c7dd2abf520a9840250700', - // messagingSenderId: '318284530945', - // projectId: 'nowwallpaper', - // storageBucket: 'nowwallpaper.appspot.com', - // ); + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCLEOFjenMfiZKdoKdGh9mylckpIpTn854', + appId: '1:280378307504:android:bcee5d63088dcbb7ca5c75', + messagingSenderId: '280378307504', + projectId: 'tonesnap-and', + storageBucket: 'tonesnap-and.appspot.com', + ); static const FirebaseOptions ios = FirebaseOptions( apiKey: 'AIzaSyADeT4ReAayii_M9tl0GHR6tUlyVTXAaSU', diff --git a/lib/firebase/firebase_remote_config_manager.dart b/lib/firebase/firebase_remote_config_manager.dart index 16adbd8..df4a7a8 100644 --- a/lib/firebase/firebase_remote_config_manager.dart +++ b/lib/firebase/firebase_remote_config_manager.dart @@ -60,6 +60,12 @@ class FirebaseRemoteConfigManager { LogUtil.e(e.toString()); } } + + // 获取 adMobLevelIDs + var adMobLevelIDs = allData['adMobLevelIDs']?.asString(); + if (ObjUtil.isNotEmpty(adMobLevelIDs)) { + await MusicBox().putAdConfig(adMobLevelIDs!); + } } } catch (e) { LogUtil.e(e.toString()); diff --git a/lib/global/app_config.dart b/lib/global/app_config.dart index 7f4e180..c0fb46d 100644 --- a/lib/global/app_config.dart +++ b/lib/global/app_config.dart @@ -24,5 +24,269 @@ class AppConfig { /// 所在区域,默认值 static String isoCode = 'HK'; + + /// iOS广告场景默认配置,level越小价值越高 + static String adIOSDefaultConfig = '''{ + "coldLoading": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/5757639118", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/5852437064", + "ad": "AdMob", + "type": "Insert" + } + ], + "hotLoading": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/3414045794", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/9382223193", + "ad": "AdMob", + "type": "Insert" + } + ], + "play": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/9827341094", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/5270553952", + "ad": "AdMob", + "type": "Insert" + } + ], + "download": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/2060531710", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/1913192057", + "ad": "AdMob", + "type": "Insert" + } + ], + "list": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/9600110380", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/6079667773", + "ad": "AdMob", + "type": "Insert" + } + ], + "library": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/7785036659", + "ad": "AdMob", + "type": "Native" + } + ], + "playCut": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/8551323416", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/2140422769", + "ad": "AdMob", + "type": "Insert" + } + ], + "search": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/8434368376", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/9505312439", + "ad": "AdMob", + "type": "Insert" + } + ], + "searchResult": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/1645766617", + "ad": "AdMob", + "type": "Native" + } + ], + "backup": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/5987502154", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/5361482476", + "ad": "AdMob", + "type": "Insert" + } + ] + }'''; + + /// Android广告场景默认配置,level越小价值越高 + static String adAndroidDefaultConfig = '''{ + "coldLoading": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/2307556684", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/3395973732", + "ad": "AdMob", + "type": "Insert" + } + ], + "hotLoading": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/2527048534", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/5763116779", + "ad": "AdMob", + "type": "Insert" + } + ], + "play": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/4450035105", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/4279113904", + "ad": "AdMob", + "type": "Insert" + } + ], + "download": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/3136953430", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/7368311678", + "ad": "AdMob", + "type": "Insert" + } + ], + "list": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/1823871765", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/2536557320", + "ad": "AdMob", + "type": "Insert" + } + ], + "library": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/5438052836", + "ad": "AdMob", + "type": "Native" + } + ], + "playCut": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/5727092792", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/8197708422", + "ad": "AdMob", + "type": "Insert" + } + ], + "search": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/4971148979", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/8026787222", + "ad": "AdMob", + "type": "Insert" + } + ], + "searchResult": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/6848602773", + "ad": "AdMob", + "type": "Native" + } + ], + "backup": [ + { + "level": 1, + "identifier": "ca-app-pub-5684307632319406/8718822297", + "ad": "AdMob", + "type": "Insert" + }, + { + "level": 2, + "identifier": "ca-app-pub-5684307632319406/4517483713", + "ad": "AdMob", + "type": "Insert" + } + ] + }'''; } diff --git a/lib/global/app_lifecycle_reactor.dart b/lib/global/app_lifecycle_reactor.dart index 9904460..4709c52 100644 --- a/lib/global/app_lifecycle_reactor.dart +++ b/lib/global/app_lifecycle_reactor.dart @@ -3,8 +3,8 @@ // Description: 应用程序生命周期反应器 import 'package:google_mobile_ads/google_mobile_ads.dart'; -import 'package:tone_snap/ads/app_open_ad_manager.dart'; import 'package:tone_snap/ads/interstitial_ad_manager.dart'; +import 'package:tone_snap/data/enum/ad_scenes.dart'; class AppLifecycleReactor { AppLifecycleReactor._(); @@ -23,7 +23,7 @@ class AppLifecycleReactor { void _onAppStateChanged(AppState appState) { // 如果应用程序正在恢复前台,尝试显示开屏广告 if (appState == AppState.foreground && !InterstitialAdManager().isShowingAd) { - AppOpenAdManager().showAdIfAvailable(); + InterstitialAdManager().showAdIfAvailable(AdScenes.hotLoading.name); } } } \ No newline at end of file diff --git a/lib/global/download_manager.dart b/lib/global/download_manager.dart index a42b801..775b630 100644 --- a/lib/global/download_manager.dart +++ b/lib/global/download_manager.dart @@ -5,11 +5,14 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:dio/dio.dart'; import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.dart'; import 'package:tone_snap/components/base_easyloading.dart'; import 'package:tone_snap/data/api/music_api.dart'; +import 'package:tone_snap/data/enum/ad_scenes.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/firebase/firebase_analytics_manager.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'; @@ -49,6 +52,11 @@ class DownloadManager extends GetxController { break; case TaskStatus.complete: LogUtil.d('音乐下载路径:${await update.task.filePath()}'); + FirebaseAnalyticsManager.logPlayerBDownloadsuccessAction( + musicModel.videoId, + musicModel.title, + musicModel.subtitle, + ); musicModel.localPath = await update.task.filePath(); OfflineBox().add(musicModel); downloadList.remove(musicModel); @@ -63,14 +71,17 @@ class DownloadManager extends GetxController { case TaskStatus.notFound: BaseEasyLoading.toast('Download failed'); downloadList.remove(musicModel); + FirebaseAnalyticsManager.logPlayerBDownloadfailAction('404,找不到url'); break; case TaskStatus.failed: BaseEasyLoading.toast('Download failed'); downloadList.remove(musicModel); + FirebaseAnalyticsManager.logPlayerBDownloadfailAction('下载失败'); break; case TaskStatus.canceled: BaseEasyLoading.toast('Download cancelled'); downloadList.remove(musicModel); + FirebaseAnalyticsManager.logPlayerBDownloadfailAction('取消下载'); break; case TaskStatus.waitingToRetry: break; @@ -89,37 +100,47 @@ class DownloadManager extends GetxController { /// 下载文件 void downloadMusic(MusicModel? m) { - if (m == null) return; - MusicModel musicModel = m.copyWith(); - musicModel.taskStatus = TaskStatus.enqueued; - musicModel.cancelToken = CancelToken(); - downloadList.add(musicModel); - updateDownloadState(); + FirebaseAnalyticsManager.logPlayerBDownloadClick( + m?.videoId, + m?.title, + m?.subtitle, + ); + InterstitialAdManager().showAdIfAvailable( + AdScenes.download.name, + onTap: () { + if (m == null) return; + MusicModel musicModel = m.copyWith(); + musicModel.taskStatus = TaskStatus.enqueued; + musicModel.cancelToken = CancelToken(); + downloadList.add(musicModel); + updateDownloadState(); - _getMusicUrl(musicModel, (url, mimeType) async { - if (ObjUtil.isNotEmpty(url)) { - String extension = mimeType ?? 'mp4'; - if (ObjUtil.isNotEmpty(mimeType)) { - // 从 mimeType 中提取主类型和子类型 - String type = mimeType!.split(';')[0].trim(); - // 获取文件扩展名 - extension = type.split('/')[1]; - } - final filename = '${musicModel.title}_${DateUtil.getNowTimestamp()}.$extension'; - final task = DownloadTask( - taskId: musicModel.videoId, - url: url!, - filename: filename, - directory: LocalPathUtil.getMusicDownloadDir(), - updates: Updates.statusAndProgress, - requiresWiFi: false, - retries: 0, - allowPause: false, - metaData: '', - ); - tq?.add(task); - } - }); + _getMusicUrl(musicModel, (url, mimeType) async { + if (ObjUtil.isNotEmpty(url)) { + String extension = mimeType ?? 'mp4'; + if (ObjUtil.isNotEmpty(mimeType)) { + // 从 mimeType 中提取主类型和子类型 + String type = mimeType!.split(';')[0].trim(); + // 获取文件扩展名 + extension = type.split('/')[1]; + } + final filename = '${musicModel.title}_${DateUtil.getNowTimestamp()}.$extension'; + final task = DownloadTask( + taskId: musicModel.videoId, + url: url!, + filename: filename, + directory: LocalPathUtil.getMusicDownloadDir(), + updates: Updates.statusAndProgress, + requiresWiFi: false, + retries: 0, + allowPause: false, + metaData: '', + ); + tq?.add(task); + } + }); + }, + ); } Future _getMusicUrl(MusicModel musicModel, Function(String? url, String? mimeType) onTap) async { @@ -134,6 +155,7 @@ class DownloadManager extends GetxController { musicModel.taskStatus = TaskStatus.failed; musicModel.cancelToken = null; } + FirebaseAnalyticsManager.logPlayerBDownloadfailAction('${baseError.code},${baseError.message}'); downloadList.remove(musicModel); updateDownloadState(); } diff --git a/lib/global/network_connectivity_service.dart b/lib/global/network_connectivity_service.dart index 446baf5..099d473 100644 --- a/lib/global/network_connectivity_service.dart +++ b/lib/global/network_connectivity_service.dart @@ -6,10 +6,9 @@ import 'dart:async'; 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/ads/library_native_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 { @@ -22,18 +21,14 @@ class NetworkConnectivityService extends GetxService { super.onInit(); subscription = Connectivity().onConnectivityChanged.listen((List result) { LogUtil.d('当前网络连接类型:$result'); - if (result.contains(ConnectivityResult.wifi) || result.contains(ConnectivityResult.mobile)) { + if (!result.contains(ConnectivityResult.none)) { if (!isExecutedTask) { isExecutedTask = true; FirebaseRemoteConfigManager.getAll(); - if (Get.isRegistered()) { - LaunchController.to.getIsoCode(); - } - - InterstitialAdManager().loadAd(); - AppOpenAdManager().loadAd(); + InterstitialAdManager().loadAdAll(); + // LibraryNativeAdManager().loadNativeAd(); } } }); diff --git a/lib/main.dart b/lib/main.dart index b08c9bb..c7b224f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,8 +20,6 @@ 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'; -import 'package:tone_snap/utils/file_util.dart'; -import 'package:tone_snap/utils/local_path_util.dart'; import 'package:tone_snap/utils/log_util.dart'; void main() async { @@ -38,7 +36,11 @@ void main() async { } // 初始化广告 SDK - MobileAds.instance.initialize(); + try { + MobileAds.instance.initialize(); + } catch (e) { + LogUtil.e("MobileAds initialization error: $e"); + } // 初始化Hive await initHive(); @@ -59,10 +61,6 @@ void main() async { systemNavigationBarIconBrightness: Brightness.light, )); } - - // 删除缓存文件 - FileUtil.deleteAllFilesInDirectory(await LocalPathUtil.getRecordingsDir()); - FileUtil.deleteAllFilesInDirectory(await LocalPathUtil.getAssetsDir()); } class MyApp extends StatelessWidget { @@ -75,7 +73,9 @@ class MyApp extends StatelessWidget { navigatorObservers = [ GetObserver((_) { WidgetsBinding.instance.addPostFrameCallback((_) { - if (Get.currentRoute == AppRoutes.playPage) { + if (Get.currentRoute == AppRoutes.playPage + || Get.currentRoute == AppRoutes.privacy + || Get.currentRoute == AppRoutes.terms) { MusicBar().hide(); } else { if (Get.isRegistered()) { diff --git a/lib/modules/launch/launch_controller.dart b/lib/modules/launch/launch_controller.dart index d110424..e6613ba 100644 --- a/lib/modules/launch/launch_controller.dart +++ b/lib/modules/launch/launch_controller.dart @@ -2,14 +2,12 @@ import 'dart:async'; 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/ads/interstitial_ad_manager.dart'; +import 'package:tone_snap/data/enum/ad_scenes.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/firebase/firebase_analytics_manager.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/routes/app_routes.dart'; @@ -25,17 +23,12 @@ class LaunchController extends GetxController with GetSingleTickerProviderStateM /// 进度每次变化值 var changeValue = 10; - late AppLifecycleReactor _appLifecycleReactor; - @override void onInit() { super.onInit(); - _appLifecycleReactor = AppLifecycleReactor(); - _appLifecycleReactor.listenToAppStateChanges(); - - Get.put(NetworkConnectivityService()); - _startTimer(); + Get.put(NetworkConnectivityService()); + FirebaseAnalyticsManager.logLaunchPV(); } @override @@ -46,14 +39,12 @@ class LaunchController extends GetxController with GetSingleTickerProviderStateM /// 开始定时器 void _startTimer() { - _timer = Timer.periodic(Duration(milliseconds: changeValue), (Timer t) { + _timer = Timer.periodic(const Duration(milliseconds: 10), (Timer t) { if (currentProcess.value + changeValue >= timeTotal) { currentProcess.value = timeTotal; - if (currentProcess >= timeTotal) { - _stopTimer(); - _checkEnter(); - return; - } + _stopTimer(); + _checkEnter(); + return; } currentProcess.value += changeValue; }); @@ -67,7 +58,7 @@ class LaunchController extends GetxController with GetSingleTickerProviderStateM /// 修改进度变化值 void editChangeValue() { - changeValue = 3000; + changeValue = 200; } /// 校验开关和版本,决定进A还是B @@ -75,18 +66,18 @@ class LaunchController extends GetxController with GetSingleTickerProviderStateM bool isOpenedSideB = MusicBox().getIsOpenedSideB(); if (isOpenedSideB) { LogUtil.d('进入过B面'); - _openSideB(); + _openSideB('进入过B面'); } else { bool enter = MusicBox().getEnter(); String versionCode = await MusicBox().getVersionCode(); final packageInfo = await PackageInfo.fromPlatform(); if (versionCode != packageInfo.version) { LogUtil.d('版本不相同,进入B面'); - _openSideB(); + _openSideB('版本不相同'); } else { if (enter) { LogUtil.d('开关:打开'); - _openSideB(); + _openSideB('版本相同, 开关打开'); } else { LogUtil.d('开关:关闭'); _openSideA(); @@ -96,26 +87,26 @@ class LaunchController extends GetxController with GetSingleTickerProviderStateM } void _openSideA() { - AppOpenAdManager().showAdIfAvailable(onTap: () { - AppConfig.appSideEnum = AppSideEnum.sideA; - Get.offNamed(AppRoutes.initialA); - }); + InterstitialAdManager().showAdIfAvailable( + AdScenes.coldLoading.name, + onTap: () { + AppConfig.appSideEnum = AppSideEnum.sideA; + Get.offNamed(AppRoutes.initialA); + }, + ); + FirebaseAnalyticsManager.logJumpEvent('A', '版本相同,开关关闭'); } - 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!; - } + void _openSideB(String reason) { + InterstitialAdManager().showAdIfAvailable( + AdScenes.coldLoading.name, + onTap: () { + AppConfig.appSideEnum = AppSideEnum.sideB; + MainController.to.changeTheme(); + Get.offNamed(AppRoutes.initialB); + MusicBox().putIsOpenedSideB(true); + }, + ); + FirebaseAnalyticsManager.logJumpEvent('B', reason); } } diff --git a/lib/modules/launch/launch_view.dart b/lib/modules/launch/launch_view.dart index 435b700..fa85b00 100644 --- a/lib/modules/launch/launch_view.dart +++ b/lib/modules/launch/launch_view.dart @@ -4,7 +4,6 @@ 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/launch/launch_controller.dart'; -import 'package:tone_snap/res/themes/app_colors.dart'; class LaunchView extends StatelessWidget { LaunchView({super.key}); diff --git a/lib/modules/sidea/change_voice/change_voice_controller.dart b/lib/modules/sidea/change_voice/change_voice_controller.dart index c7f1d56..acec0d9 100644 --- a/lib/modules/sidea/change_voice/change_voice_controller.dart +++ b/lib/modules/sidea/change_voice/change_voice_controller.dart @@ -4,6 +4,7 @@ import 'package:ffmpeg_kit_flutter_audio/return_code.dart'; import 'package:get/get.dart'; import 'package:tone_snap/ads/interstitial_ad_manager.dart'; import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/data/enum/ad_scenes.dart'; import 'package:tone_snap/data/models/voice_model.dart'; import 'package:tone_snap/data/storage/my_voice_box.dart'; import 'package:tone_snap/generated/assets.dart'; @@ -46,8 +47,6 @@ class ChangeVoiceController extends GetxController { @override void onInit() { super.onInit(); - InterstitialAdManager().loadAd(); - filePath = Get.arguments; // playerController.setFilePath(filePath); } @@ -104,9 +103,10 @@ class ChangeVoiceController extends GetxController { /// 保存 Future save() async { - // 显示插页广告 InterstitialAdManager().showAdIfAvailable( + AdScenes.download.name, onTap: () async { + await playerController.startPlay(); // 停止播放 if (playerController.isPlaying.value) playerController.stopPlay(); BaseEasyLoading.loading(); diff --git a/lib/modules/sidea/home/home_controller.dart b/lib/modules/sidea/home/home_controller.dart index 4dc7258..ab13302 100644 --- a/lib/modules/sidea/home/home_controller.dart +++ b/lib/modules/sidea/home/home_controller.dart @@ -1,5 +1,7 @@ import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.dart'; import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/data/enum/ad_scenes.dart'; import 'package:tone_snap/data/models/voice_model.dart'; import 'package:tone_snap/generated/assets.dart'; import 'package:tone_snap/modules/sidea/controllers/player_controller.dart'; @@ -22,22 +24,32 @@ class HomeController extends GetxController { ]; void onTapItem(VoiceModel item) { - InitialController.to.currentPlayVoiceModel.value = item; - Get.toNamed(AppRoutes.playSound, arguments: item); + InterstitialAdManager().showAdIfAvailable( + AdScenes.play.name, + onTap: () { + InitialController.to.currentPlayVoiceModel.value = item; + Get.toNamed(AppRoutes.playSound, arguments: item); + }, + ); } Future onTapPlayBarPlay(VoiceModel item) async { if (isPlayItem(item)) { await playerController.pausePlay(); } else { - if (!identical(item, InitialController.to.currentPlayVoiceModel.value)) { - BaseEasyLoading.loading(); - await playerController.setFilePath(item.path); - BaseEasyLoading.dismiss(); - InitialController.to.currentPlayVoiceModel.value = item; - InitialController.to.isFavourite.value = InitialController.to.getIsFavouriteModel() != null; - } - await playerController.startPlay(); + InterstitialAdManager().showAdIfAvailable( + AdScenes.play.name, + onTap: () async { + if (!identical(item, InitialController.to.currentPlayVoiceModel.value)) { + BaseEasyLoading.loading(); + await playerController.setFilePath(item.path); + BaseEasyLoading.dismiss(); + InitialController.to.currentPlayVoiceModel.value = item; + InitialController.to.isFavourite.value = InitialController.to.getIsFavouriteModel() != null; + } + await playerController.startPlay(); + }, + ); } } diff --git a/lib/modules/sidea/initial/initial_controller.dart b/lib/modules/sidea/initial/initial_controller.dart index 278b9d8..5b1c5a4 100644 --- a/lib/modules/sidea/initial/initial_controller.dart +++ b/lib/modules/sidea/initial/initial_controller.dart @@ -4,6 +4,7 @@ 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'; @@ -12,6 +13,8 @@ import 'package:tone_snap/modules/sidea/me/me_view.dart'; import 'package:tone_snap/modules/sidea/my_voice/my_voice_controller.dart'; import 'package:tone_snap/modules/sidea/settings/settings_view.dart'; import 'package:tone_snap/routes/app_routes.dart'; +import 'package:tone_snap/utils/file_util.dart'; +import 'package:tone_snap/utils/local_path_util.dart'; class InitialController extends GetxController { static InitialController get to => Get.find(); @@ -29,17 +32,27 @@ class InitialController extends GetxController { /// 是否收藏 var isFavourite = false.obs; + late AppLifecycleReactor _appLifecycleReactor; + @override void onInit() { super.onInit(); AppTrackingTransparencyManager().requestATT(); + + _appLifecycleReactor = AppLifecycleReactor(); + _appLifecycleReactor.listenToAppStateChanges(); + pageController = PageController(initialPage: currentIndex.value); } @override void onReady() async { super.onReady(); - _addEventLog(); + // 删除缓存文件 + FileUtil.deleteAllFilesInDirectory(await LocalPathUtil.getRecordingsDir()); + FileUtil.deleteAllFilesInDirectory(await LocalPathUtil.getAssetsDir()); + + FirebaseAnalyticsManager.logHomeAPV(); } @override @@ -55,10 +68,10 @@ class InitialController extends GetxController { } else { currentIndex.value = index; pageController.jumpToPage(index); - if (index == 0) { - _addEventLog(); - } if (index == 2) _refreshMe(); + if (index == 0) { + FirebaseAnalyticsManager.logHomeAPV(); + } } } @@ -95,11 +108,6 @@ class InitialController extends GetxController { if (Get.isRegistered()) MyVoiceController.to.getData(); if (Get.isRegistered()) FavouriteController.to.getData(); } - - /// 埋点 - void _addEventLog() { - FirebaseAnalyticsManager.logEvent(FirebaseAnalyticsManager.homeApv); - } } class PageItem { 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 index 7824d78..2a7fe91 100644 --- 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 @@ -1,4 +1,3 @@ -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'; @@ -19,7 +18,7 @@ class AddToPlaylistBottomSheetController extends GetxController { } void _getList() { - playlists.value = PlaylistsBox().getReversedList(); + playlists.value = PlaylistsBox().getList(); } void setMusicModel(MusicModel musicModel) { 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 index c9d6a94..c16b10a 100644 --- a/lib/modules/sideb/album_song_list/album_song_list_controller.dart +++ b/lib/modules/sideb/album_song_list/album_song_list_controller.dart @@ -1,12 +1,13 @@ -import 'dart:ffi'; - import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.dart'; import 'package:tone_snap/components/base_easyloading.dart'; import 'package:tone_snap/components/view_state_widget.dart'; import 'package:tone_snap/data/api/music_api.dart'; +import 'package:tone_snap/data/enum/ad_scenes.dart'; import 'package:tone_snap/data/models/browse_model.dart'; import 'package:tone_snap/data/models/music_model.dart'; import 'package:tone_snap/data/storage/collect_playlists_box.dart'; +import 'package:tone_snap/firebase/firebase_analytics_manager.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'; @@ -103,6 +104,9 @@ class AlbumSongListController extends GetxController { if (thumbnails != null && thumbnails.isNotEmpty) { musicModel.coverUrl = thumbnails.last.url; } + if (ObjUtil.isEmpty(musicModel.coverUrl)) { + musicModel.coverUrl = coverUrl.value; + } var flexColumns = o.musicResponsiveListItemRenderer?.flexColumns; if (flexColumns != null && flexColumns.isNotEmpty) { @@ -142,21 +146,25 @@ class AlbumSongListController extends GetxController { /// 点击播放全部歌曲 Future onTapPlayAll(bool isShuffle) async { if (musicList.isNotEmpty) { - int index = 0; - if (isShuffle && musicList.length > 1) { - final n = musicList.indexWhere((e) => e.videoId == musicPlayerController.getMusicModel()?.videoId); - if (n != -1) { - index = NumUtil.getRandomNumberExcludingCurrent(0, musicList.length, n); - } else { - index = NumUtil.getRandomNumber(0, musicList.length); - } - } - for (var o in musicList) { - if (ObjUtil.isEmpty(o.coverUrl)) { - o.coverUrl = coverUrl.value; - } - } - musicPlayerController.playMusic(musicList[index].videoId, playList: musicList); + InterstitialAdManager().showAdIfAvailable( + AdScenes.play.name, + onTap: () { + int index = 0; + if (isShuffle && musicList.length > 1) { + final n = musicList.indexWhere((e) => e.videoId == musicPlayerController.getMusicModel()?.videoId); + if (n != -1) { + index = NumUtil.getRandomNumberExcludingCurrent(0, musicList.length, n); + } else { + index = NumUtil.getRandomNumber(0, musicList.length); + } + } + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': musicList[index].videoId, + 'playList': musicList, + }); + }, + ); + FirebaseAnalyticsManager.logSongClick('来自专辑/歌单'); } } @@ -186,9 +194,15 @@ class AlbumSongListController extends GetxController { /// 点击列表 void onTapItem(MusicModel musicModel) { - Get.toNamed(AppRoutes.playPage, arguments: { - 'videoId': musicModel.videoId, - 'playlistId': musicModel.playlistId, - }); + InterstitialAdManager().showAdIfAvailable( + AdScenes.play.name, + onTap: () { + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': musicModel.videoId, + 'playList': musicList, + }); + }, + ); + FirebaseAnalyticsManager.logSongClick('来自专辑/歌单'); } } diff --git a/lib/modules/sideb/controllers/music_player_controller.dart b/lib/modules/sideb/controllers/music_player_controller.dart index 8370b84..a8a6dbe 100644 --- a/lib/modules/sideb/controllers/music_player_controller.dart +++ b/lib/modules/sideb/controllers/music_player_controller.dart @@ -15,6 +15,7 @@ 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/firebase/firebase_analytics_manager.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'; @@ -67,6 +68,9 @@ class MusicPlayerController extends GetxController { /// 当前播放的歌曲索引 var currentIndex = 0.obs; + /// 播放成功打点时的videoId,防止同一首歌播放状态监听重复打点 + String? _logVideoId; + @override void onInit() { super.onInit(); @@ -100,15 +104,18 @@ class MusicPlayerController extends GetxController { /// 播放歌曲 Future _startPlay() async { if (playlist.isNotEmpty) { + FirebaseAnalyticsManager.logPlayerBpv(getMusicModel()?.videoId, getMusicModel()?.title, getMusicModel()?.subtitle); resetPlaybackStatus(); Get.currentRoute == AppRoutes.playPage ? MusicBar().hide() : MusicBar().show(); try { + DateTime startLoadDateTime = DateTime.now(); var model = OfflineBox().getList().firstWhereOrNull((e) => e.videoId == getMusicModel()?.videoId); if (model != null && ObjUtil.isNotEmpty(model.localPath)) { // 有下载 LogUtil.d('读取下载路径=${model.localPath}'); if (!await File(model.localPath!).exists()) { BaseEasyLoading.toast('file does not exist'); + FirebaseAnalyticsManager.logPlayerBFailAction('本地文件不存在'); return; } await _player.setFilePath(model.localPath!); @@ -136,9 +143,20 @@ class MusicPlayerController extends GetxController { _cancelListening(); _addListening(); _player.play(); + + DateTime endLoadDateTime = DateTime.now(); + // 获取毫秒差 + int millisecondsDifference = endLoadDateTime.difference(startLoadDateTime).inMilliseconds; + FirebaseAnalyticsManager.logPlayerBDelayAction( + getMusicModel()?.videoId, + getMusicModel()?.title, + getMusicModel()?.subtitle, + '${millisecondsDifference}ms', + ); } catch (e) { LogUtil.e('Play failed: $e'); BaseEasyLoading.toast('Play failed'); + FirebaseAnalyticsManager.logPlayerBFailAction(e.toString()); } } } @@ -200,6 +218,16 @@ class MusicPlayerController extends GetxController { } break; } + if (isPlaying.value) { + if (_logVideoId != getMusicModel()?.videoId) { + _logVideoId = getMusicModel()?.videoId; + FirebaseAnalyticsManager.logPlayerBSuccessAction( + getMusicModel()?.videoId, + getMusicModel()?.title, + getMusicModel()?.subtitle, + ); + } + } }); } diff --git a/lib/modules/sideb/custom_playlist/custom_playlist_controller.dart b/lib/modules/sideb/custom_playlist/custom_playlist_controller.dart index c220ca2..472c1f3 100644 --- a/lib/modules/sideb/custom_playlist/custom_playlist_controller.dart +++ b/lib/modules/sideb/custom_playlist/custom_playlist_controller.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.dart'; import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/enum/ad_scenes.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/firebase/firebase_analytics_manager.dart'; import 'package:tone_snap/modules/sideb/controllers/music_player_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'; @@ -64,16 +67,25 @@ class CustomPlaylistController extends GetxController { Future onTapPlayAll(bool isShuffle) async { if (musicList.isNotEmpty) { - int index = 0; - if (isShuffle && musicList.length > 1) { - final n = musicList.indexWhere((e) => e.videoId == musicPlayerController.getMusicModel()?.videoId); - if (n != -1) { - index = NumUtil.getRandomNumberExcludingCurrent(0, musicList.length, n); - } else { - index = NumUtil.getRandomNumber(0, musicList.length); - } - } - musicPlayerController.playMusic(musicList[index].videoId, playList: musicList); + InterstitialAdManager().showAdIfAvailable( + AdScenes.play.name, + onTap: () { + int index = 0; + if (isShuffle && musicList.length > 1) { + final n = musicList.indexWhere((e) => e.videoId == musicPlayerController.getMusicModel()?.videoId); + if (n != -1) { + index = NumUtil.getRandomNumberExcludingCurrent(0, musicList.length, n); + } else { + index = NumUtil.getRandomNumber(0, musicList.length); + } + } + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': musicList[index].videoId, + 'playList': musicList, + }); + }, + ); + FirebaseAnalyticsManager.logSongClick('来自自建歌单列表'); } } @@ -103,9 +115,15 @@ class CustomPlaylistController extends GetxController { } void onTapItem(MusicModel musicModel) { - Get.toNamed(AppRoutes.playPage, arguments: { - 'videoId': musicModel.videoId, - 'playList': musicList.toList(), - }); + InterstitialAdManager().showAdIfAvailable( + AdScenes.play.name, + onTap: () { + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': musicModel.videoId, + 'playList': musicList, + }); + }, + ); + FirebaseAnalyticsManager.logSongClick('来自自建歌单列表'); } } diff --git a/lib/modules/sideb/home/home_controller.dart b/lib/modules/sideb/home/home_controller.dart index 7157a12..87b8650 100644 --- a/lib/modules/sideb/home/home_controller.dart +++ b/lib/modules/sideb/home/home_controller.dart @@ -1,13 +1,21 @@ import 'package:easy_refresh/easy_refresh.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.dart'; import 'package:tone_snap/components/view_state_widget.dart'; import 'package:tone_snap/data/api/music_api.dart'; +import 'package:tone_snap/data/api/tikustok_api.dart'; +import 'package:tone_snap/data/enum/ad_scenes.dart'; import 'package:tone_snap/data/enum/music_type.dart'; +import 'package:tone_snap/data/models/base_model.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/isocode_model.dart'; import 'package:tone_snap/data/models/music_model.dart'; +import 'package:tone_snap/data/network/base_error.dart'; +import 'package:tone_snap/firebase/firebase_analytics_manager.dart'; +import 'package:tone_snap/global/app_config.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(); @@ -42,13 +50,29 @@ class HomeController extends GetxController { firstBrowse(); } + /// 获取所在区域 + Future _getIsoCode() async { + BaseModel? model = await TikUsTokApi.getIsoCode(); + if (model != null && model.success && model.data?.isoCode != null) { + AppConfig.isoCode = model.data!.isoCode!; + } + } + /// 首次请求 Future firstBrowse() async { + await _getIsoCode(); Map queryParameters = { 'prettyPrint': false, 'browseId': 'FEmusic_home' }; - BrowseModel? browseModel = await MusicApi.browse(queryParameters: queryParameters, formJson: BrowseModel.fromMap); + BrowseModel? browseModel = await MusicApi.browse( + queryParameters: queryParameters, + formJson: BrowseModel.fromMap, + fail: (BaseError baseError) { + refreshController.finishRefresh(IndicatorResult.fail); + FirebaseAnalyticsManager.logHomeBModuleFailAction('${baseError.code},${baseError.message}'); + }, + ); if (browseModel != null) { refreshController.finishRefresh(); @@ -65,8 +89,11 @@ class HomeController extends GetxController { clickTrackingParams = continuations[0].nextContinuationData?.clickTrackingParams; } _reBrowse(continuation: continuation, clickTrackingParams: clickTrackingParams); - } else { - refreshController.finishRefresh(IndicatorResult.fail); + if (groupList.isNotEmpty) { + FirebaseAnalyticsManager.logHomeBModuleShowsuccesAction(); + } else { + FirebaseAnalyticsManager.logHomeBModuleFailAction('无数据'); + } } viewState.value = groupList.isNotEmpty ? ViewState.normal : ViewState.empty; } @@ -80,7 +107,11 @@ class HomeController extends GetxController { 'itct': clickTrackingParams, 'prettyPrint': false }; - BrowseModel? browseModel = await MusicApi.browse(visitorData: visitorData, queryParameters: queryParameters, formJson: BrowseModel.fromMap); + BrowseModel? browseModel = await MusicApi.browse( + visitorData: visitorData, + queryParameters: queryParameters, + formJson: BrowseModel.fromMap, + ); if (browseModel != null) { _extractAssemblyData(browseModel); @@ -191,6 +222,7 @@ class HomeController extends GetxController { } void openSearch() { + FirebaseAnalyticsManager.logSearchFromAction('来自首页'); Get.toNamed(AppRoutes.searchResult); } @@ -198,11 +230,18 @@ class HomeController extends GetxController { Get.toNamed(AppRoutes.setting); } - void openPlayPage(MusicModel musicModel) { - Get.toNamed(AppRoutes.playPage, arguments: { - 'videoId': musicModel.videoId, - 'playlistId': musicModel.playlistId, - }); + void openPlayPage(BrowseGroupModel browseGroupModel, MusicModel musicModel) { + InterstitialAdManager().showAdIfAvailable( + AdScenes.play.name, + onTap: () { + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': musicModel.videoId, + 'playlistId': musicModel.playlistId, + }); + }, + ); + FirebaseAnalyticsManager.logHomeBModuleClick(ObjUtil.getStr(browseGroupModel.groupTitle)); + FirebaseAnalyticsManager.logSongClick('来自首页'); } void openAlbumSong(BrowseGroupModel browseGroupModel, MusicModel musicModel) { @@ -214,5 +253,6 @@ class HomeController extends GetxController { 'title': musicModel.title, 'subtitle': musicModel.subtitle, }); + FirebaseAnalyticsManager.logHomeBModuleClick(ObjUtil.getStr(browseGroupModel.groupTitle)); } } diff --git a/lib/modules/sideb/home/home_view.dart b/lib/modules/sideb/home/home_view.dart index 80f5991..ff057c2 100644 --- a/lib/modules/sideb/home/home_view.dart +++ b/lib/modules/sideb/home/home_view.dart @@ -154,7 +154,7 @@ class HomeView extends GetView { final musicModel = browseGroupModel.browseList![index]; return BrowseItemAtv( musicModel: musicModel, - onTap: () => controller.openPlayPage(musicModel), + onTap: () => controller.openPlayPage(browseGroupModel, musicModel), ); }, ), diff --git a/lib/modules/sideb/initial/initial_controller.dart b/lib/modules/sideb/initial/initial_controller.dart index 77b81e3..87c9e49 100644 --- a/lib/modules/sideb/initial/initial_controller.dart +++ b/lib/modules/sideb/initial/initial_controller.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.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/sideb/home/home_view.dart'; import 'package:tone_snap/modules/sideb/personal_music_library/personal_music_library_controller.dart'; @@ -17,11 +19,19 @@ class InitialController extends GetxController { ]; var currentIndex = 0.obs; + late AppLifecycleReactor _appLifecycleReactor; + @override void onInit() { super.onInit(); AppTrackingTransparencyManager().requestATT(); + + _appLifecycleReactor = AppLifecycleReactor(); + _appLifecycleReactor.listenToAppStateChanges(); + pageController = PageController(initialPage: currentIndex.value); + + FirebaseAnalyticsManager.logHomeBPV(); } @override @@ -33,12 +43,18 @@ class InitialController extends GetxController { Future onBottomAppBarItemChanged(int index) async { currentIndex.value = index; pageController.jumpToPage(index); - + if (index == 0) { + FirebaseAnalyticsManager.logHomeBPV(); + } + if (index == 1) { + FirebaseAnalyticsManager.logSearchPV(); + } if (index == 2) { if (Get.isRegistered()) { PersonalMusicLibraryController.to.refreshLoveSongs(); PersonalMusicLibraryController.to.refreshOffline(); } + FirebaseAnalyticsManager.logMeBPV(); } } } diff --git a/lib/modules/sideb/love_songs/love_songs_controller.dart b/lib/modules/sideb/love_songs/love_songs_controller.dart index 1a8b65d..78e2d82 100644 --- a/lib/modules/sideb/love_songs/love_songs_controller.dart +++ b/lib/modules/sideb/love_songs/love_songs_controller.dart @@ -1,7 +1,10 @@ import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.dart'; import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/enum/ad_scenes.dart'; import 'package:tone_snap/data/models/music_model.dart'; import 'package:tone_snap/data/storage/love_songs_box.dart'; +import 'package:tone_snap/firebase/firebase_analytics_manager.dart'; import 'package:tone_snap/routes/app_routes.dart'; class LoveSongsController extends GetxController { @@ -21,9 +24,15 @@ class LoveSongsController extends GetxController { } void onTapItem(MusicModel musicModel) { - Get.toNamed(AppRoutes.playPage, arguments: { - 'videoId': musicModel.videoId, - 'playList': LoveSongsBox().getReversedList(), - }); + InterstitialAdManager().showAdIfAvailable( + AdScenes.play.name, + onTap: () { + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': musicModel.videoId, + 'playList': loveList, + }); + }, + ); + FirebaseAnalyticsManager.logSongClick('来自收藏列表'); } } 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 index 4426e78..5ad8c5a 100644 --- a/lib/modules/sideb/more_bottom_sheet/more_bottom_sheet_controller.dart +++ b/lib/modules/sideb/more_bottom_sheet/more_bottom_sheet_controller.dart @@ -6,6 +6,7 @@ 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/firebase/firebase_analytics_manager.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'; @@ -33,9 +34,19 @@ class MoreBottomSheetController extends GetxController { void onTapLove() async { if (ObjUtil.isEmpty(musicModel.videoId)) return; if (isLove.value) { + FirebaseAnalyticsManager.logPlayerBUnloveClick( + musicModel.videoId, + musicModel.title, + musicModel.subtitle, + ); await LoveSongsBox().delete(musicModel.videoId!); BaseEasyLoading.toast('Removed'); } else { + FirebaseAnalyticsManager.logPlayerBLoveClick( + musicModel.videoId, + musicModel.title, + musicModel.subtitle, + ); await LoveSongsBox().add(musicModel.copyWith()); BaseEasyLoading.toast('Collected'); } diff --git a/lib/modules/sideb/music_bar/music_bar_controller.dart b/lib/modules/sideb/music_bar/music_bar_controller.dart index 517062b..06bbdeb 100644 --- a/lib/modules/sideb/music_bar/music_bar_controller.dart +++ b/lib/modules/sideb/music_bar/music_bar_controller.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.dart'; +import 'package:tone_snap/data/enum/ad_scenes.dart'; import 'package:tone_snap/routes/app_routes.dart'; class MusicBarController extends GetxController with GetSingleTickerProviderStateMixin { @@ -28,8 +30,13 @@ class MusicBarController extends GetxController with GetSingleTickerProviderStat /// 打开播放页面 void openPlayPage() { - Get.toNamed(AppRoutes.playPage, arguments: { - 'isFormMusicBar': true, - }); + InterstitialAdManager().showAdIfAvailable( + AdScenes.play.name, + onTap: () { + Get.toNamed(AppRoutes.playPage, arguments: { + 'isFormMusicBar': true, + }); + }, + ); } } diff --git a/lib/modules/sideb/offline/offline_controller.dart b/lib/modules/sideb/offline/offline_controller.dart index d263f68..05fa962 100644 --- a/lib/modules/sideb/offline/offline_controller.dart +++ b/lib/modules/sideb/offline/offline_controller.dart @@ -1,7 +1,10 @@ import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.dart'; import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/enum/ad_scenes.dart'; import 'package:tone_snap/data/models/music_model.dart'; import 'package:tone_snap/data/storage/offline_box.dart'; +import 'package:tone_snap/firebase/firebase_analytics_manager.dart'; import 'package:tone_snap/routes/app_routes.dart'; class OfflineController extends GetxController { @@ -21,9 +24,15 @@ class OfflineController extends GetxController { } void onTapItem(MusicModel model) { - Get.toNamed(AppRoutes.playPage, arguments: { - 'videoId': model.videoId, - 'playList': OfflineBox().getReversedList(), - }); + InterstitialAdManager().showAdIfAvailable( + AdScenes.play.name, + onTap: () { + Get.toNamed(AppRoutes.playPage, arguments: { + 'videoId': model.videoId, + 'playList': offlineList, + }); + }, + ); + FirebaseAnalyticsManager.logSongClick('来自离线列表'); } } 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 fe9c389..897e1ff 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,11 +1,16 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.dart'; +import 'package:tone_snap/data/enum/ad_scenes.dart'; import 'package:tone_snap/data/storage/love_songs_box.dart'; import 'package:tone_snap/data/storage/offline_box.dart'; +import 'package:tone_snap/firebase/firebase_analytics_manager.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'; +import '../../../ads/library_native_ad_manager.dart'; + class PersonalMusicLibraryController extends GetxController with GetSingleTickerProviderStateMixin { static PersonalMusicLibraryController get to => Get.find(); var loveSongsCoverUrl = ''.obs; @@ -32,19 +37,37 @@ class PersonalMusicLibraryController extends GetxController with GetSingleTicker @override void onClose() { tabController.dispose(); + LibraryNativeAdManager().nativeAd?.dispose(); super.onClose(); } void onTapModule(int index) { + String routeName; if (index == 0) { - Get.toNamed(AppRoutes.loveSongs); - } - if (index == 1) { - Get.toNamed(AppRoutes.artists); - } - if (index == 2) { - Get.toNamed(AppRoutes.offline); + routeName = AppRoutes.loveSongs; + FirebaseAnalyticsManager.logLibraryClick( + '曲库页面', + '收藏歌曲', + ); + } else if (index == 1) { + routeName = AppRoutes.artists; + FirebaseAnalyticsManager.logLibraryClick( + '曲库页面', + '收藏艺术家', + ); + } else { + routeName = AppRoutes.offline; + FirebaseAnalyticsManager.logLibraryClick( + '曲库页面', + '离线列表', + ); } + InterstitialAdManager().showAdIfAvailable( + AdScenes.list.name, + onTap: () { + Get.toNamed(routeName); + }, + ); } void refreshLoveSongs() { 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 4663608..53339c5 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 @@ -1,6 +1,8 @@ import 'package:flutter/material.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/ads/library_native_ad_manager.dart'; import 'package:tone_snap/components/keep_alive_wrapper.dart'; import 'package:tone_snap/components/my_custom_indicator.dart'; import 'package:tone_snap/components/network_image_widget.dart'; @@ -20,12 +22,14 @@ class PersonalMusicLibraryView extends GetView { return Stack( children: [ _buildPageBg(), - SafeArea( + Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTitle(), _buildModule(), + // _buildAdWidget(), _buildTabBar(), _buildTabBarView(), ], @@ -57,9 +61,25 @@ class PersonalMusicLibraryView extends GetView { ); } + Widget _buildAdWidget() { + return Obx(() { + if (LibraryNativeAdManager().nativeAdIsLoaded.value + && LibraryNativeAdManager().nativeAd != null) { + return Container( + width: 1.sw, + height: 320.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: AdWidget(ad: LibraryNativeAdManager().nativeAd!), + ); + } else { + return Container(); + } + }); + } + Widget _buildModule() { return Padding( - padding: EdgeInsets.symmetric(vertical: 28.h, horizontal: 16.w), + padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 16.w), child: Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/modules/sideb/play_page/play_page_controller.dart b/lib/modules/sideb/play_page/play_page_controller.dart index 64d4dc8..f48fc62 100644 --- a/lib/modules/sideb/play_page/play_page_controller.dart +++ b/lib/modules/sideb/play_page/play_page_controller.dart @@ -1,12 +1,15 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.dart'; import 'package:tone_snap/components/base_easyloading.dart'; import 'package:tone_snap/components/dialog/remind_dialog.dart'; import 'package:tone_snap/data/api/music_api.dart'; +import 'package:tone_snap/data/enum/ad_scenes.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/firebase/firebase_analytics_manager.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'; @@ -30,9 +33,13 @@ class PlayPageController extends GetxController { musicPlayerController.playlist.clear(); musicPlayerController.resetPlaybackStatus(); playList = await _next(videoId, playlistId); + if (playList.isNotEmpty) { + FirebaseAnalyticsManager.logPlayerBList(); + } } musicPlayerController.playMusic(videoId, playList: playList); } + FirebaseAnalyticsManager.logPlayerBImp(); } /// 获取播放列表 @@ -91,9 +98,19 @@ class PlayPageController extends GetxController { if (ObjUtil.isEmpty(musicPlayerController.getMusicModel()?.videoId)) return; final isLove = LoveSongsBox().checkLove(musicPlayerController.getMusicModel()!.videoId); if (isLove) { + FirebaseAnalyticsManager.logPlayerBUnloveClick( + musicPlayerController.getMusicModel()?.videoId, + musicPlayerController.getMusicModel()?.title, + musicPlayerController.getMusicModel()?.subtitle, + ); await LoveSongsBox().delete(musicPlayerController.getMusicModel()!.videoId!); BaseEasyLoading.toast('Removed'); } else { + FirebaseAnalyticsManager.logPlayerBLoveClick( + musicPlayerController.getMusicModel()?.videoId, + musicPlayerController.getMusicModel()?.title, + musicPlayerController.getMusicModel()?.subtitle, + ); await LoveSongsBox().add(musicPlayerController.getMusicModel()!.copyWith()); BaseEasyLoading.toast('Collected'); } @@ -103,6 +120,7 @@ class PlayPageController extends GetxController { update([loveStateId]); } + /// 下载 void onTapDownload() { if (OfflineBox().checkDownloaded(musicPlayerController.getMusicModel()?.videoId)) { Get.dialog( @@ -141,4 +159,24 @@ class PlayPageController extends GetxController { musicPlayerController.currentIndex.value--; } } + + /// 点击上一首 + void onTapPreviousTrack() { + InterstitialAdManager().showAdIfAvailable( + AdScenes.playCut.name, + onTap: () { + musicPlayerController.previousTrack(); + }, + ); + } + + /// 点击下一首 + void onTapNextTrack() { + InterstitialAdManager().showAdIfAvailable( + AdScenes.playCut.name, + onTap: () { + musicPlayerController.nextTrack(); + }, + ); + } } \ 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 e8ddbc1..78c1c11 100644 --- a/lib/modules/sideb/play_page/play_page_view.dart +++ b/lib/modules/sideb/play_page/play_page_view.dart @@ -355,7 +355,7 @@ class PlayPageView extends StatelessWidget { child: Material( color: Colors.transparent, child: InkWell( - onTap: musicPlayerController.previousTrack, + onTap: controller.onTapPreviousTrack, child: Padding( padding: const EdgeInsets.all(10).w, child: Image.asset( @@ -411,7 +411,7 @@ class PlayPageView extends StatelessWidget { child: Material( color: Colors.transparent, child: InkWell( - onTap: musicPlayerController.nextTrack, + onTap: controller.onTapNextTrack, child: Padding( padding: const EdgeInsets.all(10).w, child: Image.asset( diff --git a/lib/modules/sideb/playlists/playlists_controller.dart b/lib/modules/sideb/playlists/playlists_controller.dart index d152c0b..d3feda7 100644 --- a/lib/modules/sideb/playlists/playlists_controller.dart +++ b/lib/modules/sideb/playlists/playlists_controller.dart @@ -4,6 +4,7 @@ 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'; +import 'package:tone_snap/firebase/firebase_analytics_manager.dart'; class PlaylistsController extends GetxController { static PlaylistsController get to => Get.find(); @@ -24,7 +25,7 @@ class PlaylistsController extends GetxController { } void getList() { - playLists.value = PlaylistsBox().getReversedList(); + playLists.value = PlaylistsBox().getList(); viewState.value = playLists.isNotEmpty ? ViewState.normal : ViewState.empty; } @@ -35,6 +36,7 @@ class PlaylistsController extends GetxController { onTap: (value) async { await PlaylistsBox().add(value); getList(); + FirebaseAnalyticsManager.logCreateList(); }, ), ); diff --git a/lib/modules/sideb/search_music/search_music_controller.dart b/lib/modules/sideb/search_music/search_music_controller.dart index 4dc90d1..c7bf937 100644 --- a/lib/modules/sideb/search_music/search_music_controller.dart +++ b/lib/modules/sideb/search_music/search_music_controller.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'package:tone_snap/data/storage/music_box.dart'; +import 'package:tone_snap/firebase/firebase_analytics_manager.dart'; import 'package:tone_snap/routes/app_routes.dart'; class SearchMusicController extends GetxController { @@ -27,6 +28,7 @@ class SearchMusicController extends GetxController { } void openSearch() { + FirebaseAnalyticsManager.logSearchFromAction('来自搜索页面'); Get.toNamed(AppRoutes.searchResult); } } diff --git a/lib/modules/sideb/search_result/search_result_controller.dart b/lib/modules/sideb/search_result/search_result_controller.dart index 5196583..75bf2d2 100644 --- a/lib/modules/sideb/search_result/search_result_controller.dart +++ b/lib/modules/sideb/search_result/search_result_controller.dart @@ -1,14 +1,18 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.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/ad_scenes.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/network/base_error.dart'; import 'package:tone_snap/data/storage/music_box.dart'; +import 'package:tone_snap/firebase/firebase_analytics_manager.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'; @@ -83,6 +87,9 @@ class SearchResultController extends GetxController with GetTickerProviderStateM } } searchSuggestionsViewState.value = ViewState.normal; + if (searchSuggestionsList.isNotEmpty) { + FirebaseAnalyticsManager.logSearchSugShow(); + } } void cleanTextEditingController() { @@ -114,6 +121,8 @@ class SearchResultController extends GetxController with GetTickerProviderStateM if (ObjUtil.isEmpty(textEditingController.text)) { return; } + FirebaseAnalyticsManager.logSearchSugClick(value); + await _cleanTab(); if (isSearchSuggestionsItem) { textEditingController.text = value; } @@ -121,11 +130,19 @@ class SearchResultController extends GetxController with GetTickerProviderStateM if (Get.isRegistered()) { SearchMusicController.to.getList(); } - await _cleanTab(); searchResultViewState.value = ViewState.loading; await _searchPreviewResult(value); - searchResultViewState.value = tabs.length > 1 ? ViewState.normal : ViewState.empty; - tabController.value = TabController(length: tabs.length, vsync: this); + if (tabs.isNotEmpty) { + FirebaseAnalyticsManager.logSearchResultPV(); + FirebaseAnalyticsManager.logSearchResultsuccessAction(); + } + InterstitialAdManager().showAdIfAvailable( + AdScenes.search.name, + onTap: () { + searchResultViewState.value = tabs.isNotEmpty ? ViewState.normal : ViewState.empty; + tabController.value = TabController(length: tabs.length, vsync: this); + }, + ); } Future _searchPreviewResult(String keyword) async { @@ -133,7 +150,13 @@ class SearchResultController extends GetxController with GetTickerProviderStateM 'prettyPrint': false, 'query': keyword }; - SearchResultModel? searchResultModel = await MusicApi.search(queryParameters: queryParameters, formJson: SearchResultModel.fromMap); + SearchResultModel? searchResultModel = await MusicApi.search( + queryParameters: queryParameters, + formJson: SearchResultModel.fromMap, + fail: (BaseError baseError) { + FirebaseAnalyticsManager.logSearchResultfailAction('${baseError.code},${baseError.message}'); + }, + ); if (searchResultModel != null) { var tabs = searchResultModel.contents?.tabbedSearchResultsRenderer?.tabs; if (tabs != null && tabs.isNotEmpty) { @@ -266,6 +289,9 @@ class SearchResultController extends GetxController with GetTickerProviderStateM } } } + if (this.tabs.isEmpty) { + FirebaseAnalyticsManager.logSearchResultfailAction('无数据'); + } } } } 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 index 300af09..9f9d933 100644 --- a/lib/modules/sideb/search_result_child/search_result_child_controller.dart +++ b/lib/modules/sideb/search_result_child/search_result_child_controller.dart @@ -1,8 +1,10 @@ import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.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/ad_scenes.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'; @@ -183,10 +185,15 @@ class SearchResultChildController extends GetxController { void onTapItem(MusicModel musicModel) { if (musicModel.musicType == MusicType.musicVideoTypeAtv.name) { - Get.toNamed(AppRoutes.playPage, arguments: { - 'videoId': musicModel.videoId, - 'playlistId': musicModel.playlistId, - }); + InterstitialAdManager().showAdIfAvailable( + AdScenes.play.name, + onTap: () { + 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: { 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 index d80c53c..91133e2 100644 --- 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 @@ -1,4 +1,6 @@ import 'package:get/get.dart'; +import 'package:tone_snap/ads/interstitial_ad_manager.dart'; +import 'package:tone_snap/data/enum/ad_scenes.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'; @@ -6,10 +8,15 @@ 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, - }); + InterstitialAdManager().showAdIfAvailable( + AdScenes.play.name, + onTap: () { + 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: { diff --git a/pubspec.yaml b/pubspec.yaml index 06e587b..ae65efd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.4+14 +version: 1.0.5+15 environment: sdk: '>=3.4.1 <4.0.0' @@ -100,7 +100,7 @@ dependencies: google_mobile_ads: ^5.1.0 # Firebase - firebase_core: ^3.2.0 + firebase_core: ^3.3.0 firebase_analytics: ^11.2.0 firebase_crashlytics: ^4.0.3 firebase_remote_config: ^5.0.3