个人曲库
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 339 KiB |
BIN
assets/images/side_b/album_title_bg.png
Executable file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
assets/images/side_b/album_total.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/images/side_b/artists.png
Normal file
|
After Width: | Height: | Size: 864 B |
BIN
assets/images/side_b/bottom_sheet_indicator.png
Normal file
|
After Width: | Height: | Size: 183 B |
BIN
assets/images/side_b/collection_album.png
Normal file
|
After Width: | Height: | Size: 653 B |
|
Before Width: | Height: | Size: 339 KiB |
BIN
assets/images/side_b/love_solid.png
Normal file
|
After Width: | Height: | Size: 624 B |
|
Before Width: | Height: | Size: 599 B After Width: | Height: | Size: 599 B |
BIN
assets/images/side_b/not_collection_album.png
Normal file
|
After Width: | Height: | Size: 856 B |
BIN
assets/images/side_b/offline_download.png
Normal file
|
After Width: | Height: | Size: 567 B |
BIN
assets/images/side_b/personal_music_library_bg.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
assets/images/side_b/placeholder_library.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
assets/images/side_b/play_list_delete.png
Normal file
|
After Width: | Height: | Size: 343 B |
BIN
assets/images/side_b/playlists_add.png
Normal file
|
After Width: | Height: | Size: 272 B |
|
Before Width: | Height: | Size: 591 B After Width: | Height: | Size: 591 B |
@ -2,14 +2,12 @@
|
||||
// Date: 2024/6/25
|
||||
// Description: 开屏广告
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||
import 'package:tone_snap/routes/app_routes.dart';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:tone_snap/utils/log_util.dart';
|
||||
|
||||
class AppOpenAdManager {
|
||||
@ -66,16 +64,16 @@ class AppOpenAdManager {
|
||||
|
||||
/// 显示广告(如果存在且尚未显示)
|
||||
/// 如果先前缓存的广告已过期,则只加载并缓存新广告
|
||||
void showAdIfAvailable() {
|
||||
void showAdIfAvailable({Function()? onTap}) {
|
||||
if (!isAdAvailable) {
|
||||
LogUtil.d('尝试在可用之前显示广告');
|
||||
loadAd();
|
||||
|
||||
_openInitial();
|
||||
if(onTap != null) onTap();
|
||||
return;
|
||||
}
|
||||
if (_isShowingAd) {
|
||||
LogUtil.d('尝试在已显示广告的情况下显示广告');
|
||||
if(onTap != null) onTap();
|
||||
return;
|
||||
}
|
||||
if (DateTime.now().subtract(maxCacheDuration).isAfter(_appOpenLoadTime!)) {
|
||||
@ -83,8 +81,7 @@ class AppOpenAdManager {
|
||||
_appOpenAd!.dispose();
|
||||
_appOpenAd = null;
|
||||
loadAd();
|
||||
|
||||
_openInitial();
|
||||
if(onTap != null) onTap();
|
||||
return;
|
||||
}
|
||||
// 设置 fullScreenContentCallback 并显示广告
|
||||
@ -107,7 +104,7 @@ class AppOpenAdManager {
|
||||
ad.dispose();
|
||||
_appOpenAd = null;
|
||||
|
||||
_openInitial();
|
||||
if(onTap != null) onTap();
|
||||
},
|
||||
onAdDismissedFullScreenContent: (ad) {
|
||||
LogUtil.d('$ad onAdDismissedFullScreenContent');
|
||||
@ -119,7 +116,7 @@ class AppOpenAdManager {
|
||||
_appOpenAd = null;
|
||||
loadAd();
|
||||
|
||||
_openInitial();
|
||||
if(onTap != null) onTap();
|
||||
},
|
||||
onAdClicked: (ad) {
|
||||
LogUtil.d('$ad onAdClicked');
|
||||
@ -127,9 +124,4 @@ class AppOpenAdManager {
|
||||
);
|
||||
_appOpenAd!.show();
|
||||
}
|
||||
|
||||
/// 打开首个页面
|
||||
void _openInitial() {
|
||||
Get.offNamed(AppRoutes.initialA);
|
||||
}
|
||||
}
|
||||
@ -62,14 +62,16 @@ class InterstitialAdManager {
|
||||
|
||||
/// 显示广告(如果存在且尚未显示)
|
||||
/// 如果先前缓存的广告已过期,则只加载并缓存新广告
|
||||
void showAdIfAvailable() {
|
||||
void showAdIfAvailable({Function()? onTap}) {
|
||||
if (!isAdAvailable) {
|
||||
LogUtil.d('尝试在可用之前显示广告');
|
||||
loadAd();
|
||||
if(onTap != null) onTap();
|
||||
return;
|
||||
}
|
||||
if (isShowingAd) {
|
||||
LogUtil.d('尝试在已显示广告的情况下显示广告');
|
||||
if(onTap != null) onTap();
|
||||
return;
|
||||
}
|
||||
if (DateTime.now().subtract(maxCacheDuration).isAfter(_appOpenLoadTime!)) {
|
||||
@ -77,6 +79,7 @@ class InterstitialAdManager {
|
||||
_interstitialAd!.dispose();
|
||||
_interstitialAd = null;
|
||||
loadAd();
|
||||
if(onTap != null) onTap();
|
||||
return;
|
||||
}
|
||||
// 设置 fullScreenContentCallback 并显示广告
|
||||
@ -95,6 +98,8 @@ class InterstitialAdManager {
|
||||
isShowingAd = false;
|
||||
ad.dispose();
|
||||
_interstitialAd = null;
|
||||
|
||||
if(onTap != null) onTap();
|
||||
},
|
||||
onAdDismissedFullScreenContent: (ad) {
|
||||
LogUtil.d('$ad onAdDismissedFullScreenContent');
|
||||
@ -102,6 +107,8 @@ class InterstitialAdManager {
|
||||
ad.dispose();
|
||||
_interstitialAd = null;
|
||||
loadAd();
|
||||
|
||||
if(onTap != null) onTap();
|
||||
},
|
||||
onAdClicked: (ad) {
|
||||
LogUtil.d('$ad onAdClicked');
|
||||
|
||||
@ -2,11 +2,24 @@
|
||||
// Date: 2024/5/7
|
||||
// Description: 加载、进度、提示框
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:tone_snap/res/themes/app_colors.dart';
|
||||
|
||||
class BaseEasyLoading {
|
||||
static void initGlobalConfig() {
|
||||
static void configLoading() {
|
||||
EasyLoading.instance
|
||||
// 自定义加载样式
|
||||
..loadingStyle = EasyLoadingStyle.custom
|
||||
..indicatorType = EasyLoadingIndicatorType.ring
|
||||
..indicatorSize = 30
|
||||
..lineWidth = 3
|
||||
..textColor = seedColor
|
||||
..indicatorColor = seedColor
|
||||
..progressColor = seedColor
|
||||
..backgroundColor = scaffoldBgColor.withOpacity(0.8)
|
||||
..maskColor = Colors.amber
|
||||
..boxShadow = <BoxShadow>[]
|
||||
// 当loading展示的时候,是否允许用户操作.
|
||||
..userInteractions = false
|
||||
// 点击背景是否关闭.
|
||||
@ -30,7 +43,7 @@ class BaseEasyLoading {
|
||||
bool show = true,
|
||||
}) {
|
||||
EasyLoading.instance.userInteractions = false;
|
||||
if (show) EasyLoading.show(status: value, dismissOnTap: dismissOnTap);
|
||||
if (show) EasyLoading.show(status: value, dismissOnTap: true);
|
||||
}
|
||||
|
||||
static void dismiss({bool dismiss = true}) {
|
||||
|
||||
@ -3,21 +3,31 @@
|
||||
// Description: 列表滚动条
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tone_snap/res/themes/app_colors.dart';
|
||||
|
||||
class BaseScrollbar extends StatelessWidget {
|
||||
const BaseScrollbar({super.key, required this.child});
|
||||
const BaseScrollbar({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.scrollController,
|
||||
this.thumbColor,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Color? thumbColor;
|
||||
final ScrollController? scrollController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
removeBottom: true,
|
||||
child: RawScrollbar(
|
||||
thumbColor: Colors.white,
|
||||
controller: scrollController,
|
||||
thumbColor: thumbColor ?? seedColor,
|
||||
radius: const Radius.circular(8.0),
|
||||
thickness: 4.0,
|
||||
thickness: 2.0,
|
||||
thumbVisibility: false,
|
||||
child: child,
|
||||
),
|
||||
|
||||
34
lib/components/music_bar.dart
Normal file
@ -0,0 +1,34 @@
|
||||
// Author: fengshengxiong
|
||||
// Date: 2024/6/30
|
||||
// Description: 全局音乐播放栏
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/music_bar/music_bar_view.dart';
|
||||
|
||||
class MusicBar {
|
||||
static final MusicBar _instance = MusicBar._();
|
||||
factory MusicBar() => _instance;
|
||||
MusicBar._();
|
||||
|
||||
OverlayEntry? overlayEntry;
|
||||
var isShow = false.obs;
|
||||
|
||||
/// 显示音乐栏
|
||||
show() {
|
||||
if (overlayEntry == null && Get.context != null) {
|
||||
overlayEntry = OverlayEntry(builder: (BuildContext context) {
|
||||
return MusicBarView();
|
||||
});
|
||||
Overlay.of(Get.context!).insert(overlayEntry!);
|
||||
isShow.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// 隐藏音乐栏
|
||||
void hide() {
|
||||
overlayEntry?.remove();
|
||||
overlayEntry = null;
|
||||
isShow.value = false;
|
||||
}
|
||||
}
|
||||
10
lib/components/music_bar/music_bar_binding.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'music_bar_controller.dart';
|
||||
|
||||
class MusicBarBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => MusicBarController());
|
||||
}
|
||||
}
|
||||
49
lib/components/music_bar/music_bar_controller.dart
Normal file
@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/routes/app_routes.dart';
|
||||
|
||||
class MusicBarController extends GetxController with GetSingleTickerProviderStateMixin {
|
||||
static MusicBarController get to => Get.find<MusicBarController>();
|
||||
AnimationController? _controller;
|
||||
var bottom = 0.0.obs;
|
||||
double bottomPadding = 0.0;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_controller = AnimationController(vsync: this)..duration = const Duration(milliseconds: 200);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
bottomPadding = MediaQuery.of(Get.context!).padding.bottom;
|
||||
bottom.value = kBottomNavigationBarHeight + bottomPadding;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_controller?.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// 底部导航栏消失时沉底
|
||||
void toBottom() {
|
||||
var animation = Tween<double>(begin: bottom.value, end: bottomPadding).animate(_controller!);
|
||||
animation.addListener(() {
|
||||
bottom.value = animation.value;
|
||||
});
|
||||
_controller!.forward();
|
||||
}
|
||||
|
||||
/// 底部导航栏出现时抬高
|
||||
void riseUp() {
|
||||
var animation = Tween<double>(begin: bottom.value, end: kBottomNavigationBarHeight + bottomPadding).animate(_controller!);
|
||||
animation.addListener(() {
|
||||
bottom.value = animation.value;
|
||||
});
|
||||
_controller!.forward();
|
||||
}
|
||||
|
||||
/// 打开播放页面
|
||||
void openPlayPage() {
|
||||
Get.toNamed(AppRoutes.playPage, arguments: {'isMusicBarOpen': true});
|
||||
}
|
||||
}
|
||||
95
lib/components/music_bar/music_bar_view.dart
Normal file
@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/my_marquee_text.dart';
|
||||
import 'package:tone_snap/components/network_image_widget.dart';
|
||||
import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart';
|
||||
import 'package:tone_snap/res/themes/app_sizes.dart';
|
||||
import 'package:tone_snap/utils/obj_util.dart';
|
||||
|
||||
import 'music_bar_controller.dart';
|
||||
|
||||
class MusicBarView extends StatelessWidget {
|
||||
MusicBarView({super.key});
|
||||
|
||||
final controller = Get.put(MusicBarController(), permanent: true);
|
||||
final musicPlayerController = MusicPlayerController.to;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned(
|
||||
bottom: controller.bottom.value,
|
||||
left: 16.w,
|
||||
right: 16.w,
|
||||
child: GestureDetector(
|
||||
onTap: controller.openPlayPage,
|
||||
child: Container(
|
||||
width: 1.sw - 32.w,
|
||||
height: musicBarHeight,
|
||||
padding: EdgeInsets.symmetric(horizontal: 26.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF80F988),
|
||||
borderRadius: BorderRadius.circular(36).r,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x40040604),
|
||||
offset: Offset(0, 4),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipOval(
|
||||
child: Obx(() {
|
||||
return NetworkImageWidget(
|
||||
url: musicPlayerController.musicModel.value.thumbnail,
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
);
|
||||
}),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(() {
|
||||
return MyMarqueeText(
|
||||
text: ObjUtil.getStr(musicPlayerController.musicModel.value.title),
|
||||
textStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 4.h),
|
||||
Obx(() {
|
||||
return MyMarqueeText(
|
||||
text: ObjUtil.getStr(musicPlayerController.musicModel.value.subTitle),
|
||||
textStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -6,19 +6,25 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:widget_marquee/widget_marquee.dart';
|
||||
|
||||
class MyMarqueeText extends StatelessWidget {
|
||||
const MyMarqueeText({super.key, required this.text, required this.textStyle});
|
||||
const MyMarqueeText({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.textStyle,
|
||||
this.enable = true,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final TextStyle textStyle;
|
||||
final bool enable;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Marquee(
|
||||
delay: const Duration(seconds: 2),
|
||||
duration: const Duration(seconds: 6),
|
||||
duration: Duration(seconds: enable ? 12 : 0),
|
||||
pause: Duration.zero,
|
||||
gap: 60,
|
||||
gap: 80,
|
||||
child: Text(text, style: textStyle),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,10 +12,10 @@ class NetworkImageWidget extends StatelessWidget {
|
||||
this.width,
|
||||
this.height,
|
||||
this.radius = 0.0,
|
||||
required this.url,
|
||||
this.url,
|
||||
this.fit = BoxFit.cover,
|
||||
this.placeholder,
|
||||
this.errorWidget,
|
||||
this.noPlaceholder = false,
|
||||
});
|
||||
|
||||
final double? width;
|
||||
@ -23,8 +23,8 @@ class NetworkImageWidget extends StatelessWidget {
|
||||
final double radius;
|
||||
final String? url;
|
||||
final BoxFit fit;
|
||||
final Widget? placeholder;
|
||||
final Widget? errorWidget;
|
||||
final String? placeholder;
|
||||
final bool noPlaceholder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -35,23 +35,20 @@ class NetworkImageWidget extends StatelessWidget {
|
||||
height: height,
|
||||
imageUrl: '$url',
|
||||
fit: fit,
|
||||
placeholder: (context, url) {
|
||||
return placeholder ?? _placeholderWidget(Assets.sideBImgPlaceholder);
|
||||
placeholder: noPlaceholder ? null : (context, url) {
|
||||
return _placeholderWidget(Assets.sideBImgPlaceholder);
|
||||
},
|
||||
errorWidget: (context, url, error) {
|
||||
return errorWidget ?? _placeholderWidget(Assets.sideBImgError);
|
||||
errorWidget: noPlaceholder ? null : (context, url, error) {
|
||||
return _placeholderWidget(Assets.sideBImgError);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _placeholderWidget(String imgName) {
|
||||
return Container(
|
||||
color: Colors.white10,
|
||||
child: Image.asset(
|
||||
imgName,
|
||||
color: Colors.white12,
|
||||
),
|
||||
Widget _placeholderWidget(String img) {
|
||||
return Image.asset(
|
||||
placeholder ?? img,
|
||||
color: Colors.white12,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
25
lib/components/shader_mask.dart
Normal file
@ -0,0 +1,25 @@
|
||||
// Author: fengshengxiong
|
||||
// Date: 2024/6/30
|
||||
// Description: ShaderMaskWidget
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ShaderMaskWidget extends StatelessWidget {
|
||||
const ShaderMaskWidget({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShaderMask(
|
||||
shaderCallback: (Rect bounds) {
|
||||
return const LinearGradient(
|
||||
colors: [Colors.transparent, Colors.white, Colors.white, Colors.transparent],
|
||||
stops: [0.0, 0.05, 0.95, 1],
|
||||
).createShader(bounds);
|
||||
},
|
||||
blendMode: BlendMode.dstIn,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -13,10 +13,12 @@ class ViewStateWidget extends StatelessWidget {
|
||||
super.key,
|
||||
required this.viewState,
|
||||
required this.child,
|
||||
this.cpiBgColor,
|
||||
});
|
||||
|
||||
final ViewState viewState;
|
||||
final Widget child;
|
||||
final Color? cpiBgColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -24,7 +26,9 @@ class ViewStateWidget extends StatelessWidget {
|
||||
case ViewState.normal:
|
||||
return child;
|
||||
case ViewState.loading:
|
||||
return loadingView();
|
||||
return loadingView(
|
||||
backgroundColor: cpiBgColor,
|
||||
);
|
||||
case ViewState.empty:
|
||||
return emptyView();
|
||||
case ViewState.error:
|
||||
@ -42,9 +46,10 @@ Widget loadingView({
|
||||
}) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
color: color,
|
||||
valueColor: valueColor,
|
||||
backgroundColor: backgroundColor ?? Colors.white,
|
||||
backgroundColor: backgroundColor,
|
||||
value: value,
|
||||
),
|
||||
);
|
||||
|
||||
@ -3,18 +3,22 @@
|
||||
// Description: 音乐播放器Api
|
||||
|
||||
import 'package:devicelocale/devicelocale.dart';
|
||||
import 'package:tone_snap/data/app_config.dart';
|
||||
import 'package:tone_snap/data/sideb/models/browse_model.dart';
|
||||
import 'package:tone_snap/data/models/player_model.dart';
|
||||
import 'package:tone_snap/global/app_config.dart';
|
||||
import 'package:tone_snap/data/models/browse_model.dart';
|
||||
import 'package:tone_snap/data/network/dio_client.dart';
|
||||
import 'package:tone_snap/data/sideb/models/next_model.dart';
|
||||
import 'package:tone_snap/data/sideb/models/player_model.dart';
|
||||
import 'package:tone_snap/data/models/next_model.dart';
|
||||
import 'package:tone_snap/utils/date_util.dart';
|
||||
|
||||
class MusicApi {
|
||||
static const String baseUrl = 'https://music.youtube.com/youtubei/v1/';
|
||||
|
||||
/// 首页browse接口
|
||||
static Future<BrowseModel?> browse({String? visitorData, Map<String, dynamic>? queryParameters}) async {
|
||||
static Future<T?> browse<T>({
|
||||
String? visitorData,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
T Function(Map<String, dynamic>)? formJson,
|
||||
}) async {
|
||||
String date = DateUtil.getSevenDaysAgo();
|
||||
String locale = await Devicelocale.currentLocale ?? AppConfig.defaultLocale;
|
||||
final body = {
|
||||
@ -26,20 +30,21 @@ class MusicApi {
|
||||
"clientVersion": "1.20240607.01.00",
|
||||
"platform": "DESKTOP",
|
||||
"hl": locale,
|
||||
"gl": AppConfig.isoCode
|
||||
// "gl": AppConfig.isoCode
|
||||
"gl": 'HK'
|
||||
}
|
||||
}
|
||||
};
|
||||
BrowseModel? browseModel;
|
||||
await DioClient().request<BrowseModel>(
|
||||
T? resultModel;
|
||||
await DioClient().request<T>(
|
||||
'browse',
|
||||
requestMethod: RequestMethod.post,
|
||||
data: body,
|
||||
queryParameters: queryParameters,
|
||||
formJson: BrowseModel.fromMap,
|
||||
success: (model) => browseModel = model,
|
||||
formJson: formJson,
|
||||
success: (model) => resultModel = model,
|
||||
);
|
||||
return browseModel;
|
||||
return resultModel;
|
||||
}
|
||||
|
||||
/// next接口
|
||||
@ -54,7 +59,8 @@ class MusicApi {
|
||||
"clientVersion": "1.$date",
|
||||
"platform": "DESKTOP",
|
||||
"hl": locale,
|
||||
"gl": AppConfig.isoCode
|
||||
// "gl": AppConfig.isoCode
|
||||
"gl": 'HK'
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -66,6 +72,8 @@ class MusicApi {
|
||||
NextModel? nextModel;
|
||||
await DioClient().request<NextModel>(
|
||||
'next',
|
||||
showLoading: true,
|
||||
showToast: true,
|
||||
requestMethod: RequestMethod.post,
|
||||
data: body,
|
||||
queryParameters: queryParameters,
|
||||
@ -96,6 +104,8 @@ class MusicApi {
|
||||
PlayerModel? playerModel;
|
||||
await DioClient().request<PlayerModel>(
|
||||
'player',
|
||||
showLoading: true,
|
||||
showToast: true,
|
||||
requestMethod: RequestMethod.post,
|
||||
data: body,
|
||||
queryParameters: queryParameters,
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import 'package:tone_snap/data/models/base_model.dart';
|
||||
import 'package:tone_snap/data/network/dio_client.dart';
|
||||
import 'package:tone_snap/data/sideb/models/isocode_model.dart';
|
||||
import 'package:tone_snap/data/models/isocode_model.dart';
|
||||
|
||||
class TikUsTokApi {
|
||||
static const String baseUrl = 'https://api.tikustok.com/';
|
||||
11
lib/data/cache/music_cache_manager.dart
vendored
@ -1,6 +1,6 @@
|
||||
// Author: fengshengxiong
|
||||
// Date: 2024/6/25
|
||||
// Description: 音乐自定义缓存管理器
|
||||
// Description: 自定义音乐缓存管理器
|
||||
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
|
||||
@ -13,4 +13,13 @@ class MusicCacheManager {
|
||||
maxNrOfCacheObjects: 20,
|
||||
),
|
||||
);
|
||||
|
||||
static String getCacheKey(String videoId) {
|
||||
return 'music_$videoId';
|
||||
}
|
||||
|
||||
/// 检查是否有缓存
|
||||
static Future<FileInfo?> checkCache(String videoId) async {
|
||||
return await instance.getFileFromCache(getCacheKey(videoId));
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,34 @@
|
||||
// Author: fengshengxiong
|
||||
// Date: 2024/6/13
|
||||
// Description: BrowseType
|
||||
// Description: 资源类型
|
||||
|
||||
enum BrowseType {
|
||||
musicVideoTypeAtv(name: 'MUSIC_VIDEO_TYPE_ATV', remark: '电台/单曲'),
|
||||
musicVideoTypeOmv(name: 'MUSIC_VIDEO_TYPE_OMV', remark: '视频'),
|
||||
musicPageTypeAlbum(name: 'MUSIC_PAGE_TYPE_ALBUM', remark: '专辑'),
|
||||
// musicPageTypeArtist(name: 'MUSIC_PAGE_TYPE_ARTIST', remark: '艺术家'),
|
||||
musicPageTypePlaylist(name: 'MUSIC_PAGE_TYPE_PLAYLIST', remark: '歌单/列表');
|
||||
// musicPageTypeTrackLyrics(name: 'MUSIC_PAGE_TYPE_TRACK_LYRICS', remark: '歌词'),
|
||||
// musicPageTypeTrackRelated(name: 'MUSIC_PAGE_TYPE_TRACK_RELATED', remark: '相关内容');
|
||||
/// 电台/单曲
|
||||
musicVideoTypeAtv(name: 'MUSIC_VIDEO_TYPE_ATV'),
|
||||
|
||||
/// 视频
|
||||
musicVideoTypeOmv(name: 'MUSIC_VIDEO_TYPE_OMV'),
|
||||
|
||||
/// 专辑
|
||||
musicPageTypeAlbum(name: 'MUSIC_PAGE_TYPE_ALBUM'),
|
||||
|
||||
/// 艺术家
|
||||
// musicPageTypeArtist(name: 'MUSIC_PAGE_TYPE_ARTIST'),
|
||||
|
||||
/// 歌单/列表
|
||||
musicPageTypePlaylist(name: 'MUSIC_PAGE_TYPE_PLAYLIST');
|
||||
|
||||
/// 歌词
|
||||
// musicPageTypeTrackLyrics(name: 'MUSIC_PAGE_TYPE_TRACK_LYRICS'),
|
||||
|
||||
/// 相关内容
|
||||
// musicPageTypeTrackRelated(name: 'MUSIC_PAGE_TYPE_TRACK_RELATED');
|
||||
|
||||
const BrowseType({
|
||||
required this.name,
|
||||
required this.remark,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String remark;
|
||||
}
|
||||
|
||||
extension BrowseTypeExtension on BrowseType {
|
||||
19
lib/data/enum/play_mode.dart
Normal file
@ -0,0 +1,19 @@
|
||||
// Author: fengshengxiong
|
||||
// Date: 2024/6/13
|
||||
// Description: 播放模式
|
||||
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'play_mode.g.dart';
|
||||
|
||||
@HiveType(typeId: 1)
|
||||
enum PlayMode {
|
||||
@HiveField(0)
|
||||
listLoop,
|
||||
|
||||
@HiveField(1)
|
||||
random,
|
||||
|
||||
@HiveField(2)
|
||||
singleCycle;
|
||||
}
|
||||
51
lib/data/enum/play_mode.g.dart
Normal file
@ -0,0 +1,51 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'play_mode.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class PlayModeAdapter extends TypeAdapter<PlayMode> {
|
||||
@override
|
||||
final int typeId = 1;
|
||||
|
||||
@override
|
||||
PlayMode read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return PlayMode.listLoop;
|
||||
case 1:
|
||||
return PlayMode.random;
|
||||
case 2:
|
||||
return PlayMode.singleCycle;
|
||||
default:
|
||||
return PlayMode.listLoop;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, PlayMode obj) {
|
||||
switch (obj) {
|
||||
case PlayMode.listLoop:
|
||||
writer.writeByte(0);
|
||||
break;
|
||||
case PlayMode.random:
|
||||
writer.writeByte(1);
|
||||
break;
|
||||
case PlayMode.singleCycle:
|
||||
writer.writeByte(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PlayModeAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
2693
lib/data/models/browse_album_model.dart
Normal file
@ -1,6 +1,6 @@
|
||||
// Author: fengshengxiong
|
||||
// Date: 2024/6/18
|
||||
// Description: Browse接口模型
|
||||
// Description: Browse-首页模型
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
@ -550,10 +550,12 @@ class PurpleNavigationEndpoint {
|
||||
|
||||
class PurpleBrowseEndpoint {
|
||||
String? browseId;
|
||||
String? params;
|
||||
BrowseEndpointContextSupportedConfigs? browseEndpointContextSupportedConfigs;
|
||||
|
||||
PurpleBrowseEndpoint({
|
||||
this.browseId,
|
||||
this.params,
|
||||
this.browseEndpointContextSupportedConfigs,
|
||||
});
|
||||
|
||||
@ -563,11 +565,13 @@ class PurpleBrowseEndpoint {
|
||||
|
||||
factory PurpleBrowseEndpoint.fromMap(Map<String, dynamic> json) => PurpleBrowseEndpoint(
|
||||
browseId: json["browseId"],
|
||||
params: json["params"],
|
||||
browseEndpointContextSupportedConfigs: json["browseEndpointContextSupportedConfigs"] == null ? null : BrowseEndpointContextSupportedConfigs.fromMap(json["browseEndpointContextSupportedConfigs"]),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
"browseId": browseId,
|
||||
"params": params,
|
||||
"browseEndpointContextSupportedConfigs": browseEndpointContextSupportedConfigs?.toMap(),
|
||||
};
|
||||
}
|
||||
@ -39,6 +39,7 @@ class Content {
|
||||
String? videoId;
|
||||
String? playlistId;
|
||||
String? browseId;
|
||||
String? params;
|
||||
|
||||
Content({
|
||||
this.title,
|
||||
@ -47,6 +48,7 @@ class Content {
|
||||
this.videoId,
|
||||
this.playlistId,
|
||||
this.browseId,
|
||||
this.params,
|
||||
});
|
||||
|
||||
factory Content.fromJson(String str) => Content.fromMap(json.decode(str));
|
||||
@ -60,6 +62,7 @@ class Content {
|
||||
videoId: json["videoId"],
|
||||
playlistId: json["playlistId"],
|
||||
browseId: json["browseId"],
|
||||
params: json["params"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
@ -69,5 +72,6 @@ class Content {
|
||||
"videoId": videoId,
|
||||
"playlistId": playlistId,
|
||||
"browseId": browseId,
|
||||
"params": params,
|
||||
};
|
||||
}
|
||||
74
lib/data/models/music_model.dart
Normal file
@ -0,0 +1,74 @@
|
||||
// Author: fengshengxiong
|
||||
// Date: 2024/6/24
|
||||
// Description: 音乐模型
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'music_model.g.dart';
|
||||
|
||||
@HiveType(typeId: 2)
|
||||
class MusicModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
String? title;
|
||||
@HiveField(1)
|
||||
String? subTitle;
|
||||
@HiveField(2)
|
||||
String? thumbnail;
|
||||
@HiveField(3)
|
||||
String? videoId;
|
||||
@HiveField(4)
|
||||
String? playlistId;
|
||||
@HiveField(5)
|
||||
String? url;
|
||||
|
||||
MusicModel({
|
||||
this.title,
|
||||
this.subTitle,
|
||||
this.thumbnail,
|
||||
this.videoId,
|
||||
this.playlistId,
|
||||
this.url,
|
||||
});
|
||||
|
||||
MusicModel copyWith({
|
||||
String? title,
|
||||
String? subTitle,
|
||||
String? thumbnail,
|
||||
String? url,
|
||||
String? videoId,
|
||||
String? playlistId,
|
||||
}) =>
|
||||
MusicModel(
|
||||
title: title ?? this.title,
|
||||
subTitle: subTitle ?? this.subTitle,
|
||||
thumbnail: thumbnail ?? this.thumbnail,
|
||||
url: url ?? this.url,
|
||||
videoId: videoId ?? this.videoId,
|
||||
playlistId: playlistId ?? this.playlistId,
|
||||
);
|
||||
|
||||
factory MusicModel.fromJson(String str) =>
|
||||
MusicModel.fromMap(json.decode(str));
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory MusicModel.fromMap(Map<String, dynamic> json) => MusicModel(
|
||||
title: json["title"],
|
||||
subTitle: json["subTitle"],
|
||||
thumbnail: json["thumbnail"],
|
||||
videoId: json["videoId"],
|
||||
playlistId: json["playlistId"],
|
||||
url: json["url"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
"title": title,
|
||||
"subTitle": subTitle,
|
||||
"thumbnail": thumbnail,
|
||||
"videoId": videoId,
|
||||
"playlistId": playlistId,
|
||||
"url": url,
|
||||
};
|
||||
}
|
||||
56
lib/data/models/music_model.g.dart
Normal file
@ -0,0 +1,56 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'music_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class MusicModelAdapter extends TypeAdapter<MusicModel> {
|
||||
@override
|
||||
final int typeId = 2;
|
||||
|
||||
@override
|
||||
MusicModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return MusicModel(
|
||||
title: fields[0] as String?,
|
||||
subTitle: fields[1] as String?,
|
||||
thumbnail: fields[2] as String?,
|
||||
videoId: fields[3] as String?,
|
||||
playlistId: fields[4] as String?,
|
||||
url: fields[5] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, MusicModel obj) {
|
||||
writer
|
||||
..writeByte(6)
|
||||
..writeByte(0)
|
||||
..write(obj.title)
|
||||
..writeByte(1)
|
||||
..write(obj.subTitle)
|
||||
..writeByte(2)
|
||||
..write(obj.thumbnail)
|
||||
..writeByte(3)
|
||||
..write(obj.videoId)
|
||||
..writeByte(4)
|
||||
..write(obj.playlistId)
|
||||
..writeByte(5)
|
||||
..write(obj.url);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is MusicModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@ -7,7 +7,7 @@ import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
import 'package:tone_snap/components/base_easyloading.dart';
|
||||
import 'package:tone_snap/data/sideb/api/music_api.dart';
|
||||
import 'package:tone_snap/data/api/music_api.dart';
|
||||
import 'package:tone_snap/data/network/base_error.dart';
|
||||
import 'package:tone_snap/data/network/dio_interceptor.dart';
|
||||
import 'package:tone_snap/utils/log_util.dart';
|
||||
@ -61,14 +61,14 @@ class DioClient {
|
||||
/// 请求
|
||||
Future request<T>(
|
||||
String path, {
|
||||
bool showLoading = false,
|
||||
bool showToast = false,
|
||||
required RequestMethod requestMethod,
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
CancelToken? cancelToken,
|
||||
Options? options,
|
||||
bool isFormData = false,
|
||||
bool showLoading = false,
|
||||
bool showToast = false,
|
||||
T Function(Map<String, dynamic>)? formJson,
|
||||
required Function(T? result) success,
|
||||
Function(BaseError baseError)? fail,
|
||||
@ -94,7 +94,7 @@ class DioClient {
|
||||
} catch (e) {
|
||||
BaseError baseError = getError(e);
|
||||
if (fail != null) fail(baseError);
|
||||
BaseEasyLoading.toast(baseError.message);
|
||||
BaseEasyLoading.toast(baseError.message, show: showToast);
|
||||
LogUtil.e(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
// Author: fengshengxiong
|
||||
// Date: 2024/6/24
|
||||
// Description: 播放列表模型
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
class PlayListModel {
|
||||
String? title;
|
||||
String? subTitle;
|
||||
String? thumbnail;
|
||||
String? videoId;
|
||||
String? playlistId;
|
||||
String? url;
|
||||
|
||||
PlayListModel({
|
||||
this.title,
|
||||
this.subTitle,
|
||||
this.thumbnail,
|
||||
this.videoId,
|
||||
this.playlistId,
|
||||
this.url,
|
||||
});
|
||||
|
||||
factory PlayListModel.fromJson(String str) => PlayListModel.fromMap(json.decode(str));
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory PlayListModel.fromMap(Map<String, dynamic> json) => PlayListModel(
|
||||
title: json["title"],
|
||||
subTitle: json["subTitle"],
|
||||
thumbnail: json["thumbnail"],
|
||||
videoId: json["videoId"],
|
||||
playlistId: json["playlistId"],
|
||||
url: json["url"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
"title": title,
|
||||
"subTitle": subTitle,
|
||||
"thumbnail": thumbnail,
|
||||
"videoId": videoId,
|
||||
"playlistId": playlistId,
|
||||
"url": url,
|
||||
};
|
||||
}
|
||||
@ -2,18 +2,18 @@
|
||||
// Date: 2024/5/8
|
||||
// Description: 收藏数据
|
||||
|
||||
import 'package:tone_snap/data/sidea/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/storage/hive_storage.dart';
|
||||
|
||||
class FavoriteData {
|
||||
class FavoriteBox {
|
||||
/// 私有构造函数
|
||||
FavoriteData._();
|
||||
FavoriteBox._();
|
||||
|
||||
/// 静态常量用于保存类的唯一实例
|
||||
static final FavoriteData _instance = FavoriteData._();
|
||||
static final FavoriteBox _instance = FavoriteBox._();
|
||||
|
||||
/// 工厂构造函数返回类的唯一实例
|
||||
factory FavoriteData() {
|
||||
factory FavoriteBox() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
@ -3,19 +3,27 @@
|
||||
// Description: 持久化储存
|
||||
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:tone_snap/data/sidea/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/enum/play_mode.dart';
|
||||
import 'package:tone_snap/data/models/music_model.dart';
|
||||
import 'package:tone_snap/data/models/voice_model.dart';
|
||||
|
||||
const myVoiceBox = 'myVoiceBox';
|
||||
const favoriteBox = 'favoriteBox';
|
||||
const musicBox = 'musicBox';
|
||||
const loveSongsBox = 'loveSongsBox';
|
||||
|
||||
Future initHive() async {
|
||||
// 初始化
|
||||
await Hive.initFlutter();
|
||||
// 注册类型适配器
|
||||
Hive.registerAdapter(VoiceModelAdapter());
|
||||
Hive.registerAdapter(PlayModeAdapter());
|
||||
Hive.registerAdapter(MusicModelAdapter());
|
||||
// 打开盒子
|
||||
await Hive.openBox<VoiceModel>(myVoiceBox);
|
||||
await Hive.openBox<VoiceModel>(favoriteBox);
|
||||
await Hive.openBox(musicBox);
|
||||
await Hive.openBox<MusicModel>(loveSongsBox);
|
||||
}
|
||||
|
||||
Box<VoiceModel> getMyVoiceBox() {
|
||||
@ -24,4 +32,12 @@ Box<VoiceModel> getMyVoiceBox() {
|
||||
|
||||
Box<VoiceModel> getFavoriteBox() {
|
||||
return Hive.box<VoiceModel>(favoriteBox);
|
||||
}
|
||||
|
||||
Box getMusicBox() {
|
||||
return Hive.box(musicBox);
|
||||
}
|
||||
|
||||
Box<MusicModel> getLoveSongsBox() {
|
||||
return Hive.box<MusicModel>(loveSongsBox);
|
||||
}
|
||||
53
lib/data/storage/love_songs_box.dart
Normal file
@ -0,0 +1,53 @@
|
||||
// Author: fengshengxiong
|
||||
// Date: 2024/5/8
|
||||
// Description: 喜欢的歌曲
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/data/models/music_model.dart';
|
||||
import 'package:tone_snap/data/storage/hive_storage.dart';
|
||||
|
||||
class LoveSongsBox {
|
||||
LoveSongsBox._();
|
||||
|
||||
static final LoveSongsBox _instance = LoveSongsBox._();
|
||||
|
||||
factory LoveSongsBox() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/// 声明盒子
|
||||
/// 注意, main函数中这个盒子已经打开, 可以进行存储操作
|
||||
final _box = getLoveSongsBox();
|
||||
|
||||
/// 获取数据
|
||||
List<MusicModel> getList() {
|
||||
return _box.values.toList();
|
||||
}
|
||||
|
||||
/// 添加数据
|
||||
Future<int> addData(MusicModel model) async {
|
||||
return await _box.add(model);
|
||||
}
|
||||
|
||||
/// 校验该歌曲是否加入喜欢
|
||||
bool isLove(String videoId) {
|
||||
var model = getList().firstWhereOrNull((e) => e.videoId == videoId);
|
||||
return model != null;
|
||||
}
|
||||
|
||||
/// 删除
|
||||
Future<void> delete(String videoId) async {
|
||||
var list = getList();
|
||||
var model = list.firstWhereOrNull((e) => e.videoId == videoId);
|
||||
if (model != null) {
|
||||
await _box.delete(list.indexOf(model));
|
||||
await _box.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空所有数据
|
||||
Future<void> clear() async {
|
||||
await _box.clear();
|
||||
await _box.flush();
|
||||
}
|
||||
}
|
||||
30
lib/data/storage/music_box.dart
Normal file
@ -0,0 +1,30 @@
|
||||
// Author: fengshengxiong
|
||||
// Date: 2024/5/8
|
||||
// Description: 音乐数据盒子
|
||||
|
||||
import 'package:tone_snap/data/enum/play_mode.dart';
|
||||
import 'package:tone_snap/data/storage/hive_storage.dart';
|
||||
|
||||
class MusicBox {
|
||||
MusicBox._();
|
||||
|
||||
static final MusicBox _instance = MusicBox._();
|
||||
|
||||
factory MusicBox() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/// 声明盒子
|
||||
/// 注意, main函数中这个盒子已经打开, 可以进行存储操作
|
||||
final _box = getMusicBox();
|
||||
|
||||
/// 设置播放模式
|
||||
Future<void> putPlayMode(PlayMode playMode) {
|
||||
return _box.put('play_mode', playMode);
|
||||
}
|
||||
|
||||
/// 获取播放模式
|
||||
PlayMode getPlayMode() {
|
||||
return _box.get('play_mode') ?? PlayMode.listLoop;
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,19 @@
|
||||
// Author: fengshengxiong
|
||||
// Date: 2024/5/8
|
||||
// Description: 收藏数据
|
||||
// Description: 我的音频
|
||||
|
||||
import 'package:tone_snap/data/sidea/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/storage/hive_storage.dart';
|
||||
|
||||
class MyVoiceData {
|
||||
class MyVoiceBox {
|
||||
/// 私有构造函数
|
||||
MyVoiceData._();
|
||||
MyVoiceBox._();
|
||||
|
||||
/// 静态常量用于保存类的唯一实例
|
||||
static final MyVoiceData _instance = MyVoiceData._();
|
||||
static final MyVoiceBox _instance = MyVoiceBox._();
|
||||
|
||||
/// 工厂构造函数返回类的唯一实例
|
||||
factory MyVoiceData() {
|
||||
factory MyVoiceBox() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
@ -63,9 +63,12 @@ class Assets {
|
||||
static const String sideAUploadRecordSound = 'assets/images/side_a/upload_record_sound.png';
|
||||
static const String sideAUserAgreement = 'assets/images/side_a/user_agreement.png';
|
||||
static const String sideAVoiceDefault = 'assets/images/side_a/voice_default.png';
|
||||
static const String sideBAlbumTitleBg = 'assets/images/side_b/album_title_bg.png';
|
||||
static const String sideBAlbumTotal = 'assets/images/side_b/album_total.png';
|
||||
static const String sideBArrowDownBack = 'assets/images/side_b/arrow_down_back.png';
|
||||
static const String sideBArrowLeftBack = 'assets/images/side_b/arrow_left_back.png';
|
||||
static const String sideBArrowRightItem = 'assets/images/side_b/arrow_right_item.png';
|
||||
static const String sideBArtists = 'assets/images/side_b/artists.png';
|
||||
static const String sideBBnb1Selected = 'assets/images/side_b/bnb1_selected.png';
|
||||
static const String sideBBnb1Unselected = 'assets/images/side_b/bnb1_unselected.png';
|
||||
static const String sideBBnb2Selected = 'assets/images/side_b/bnb2_selected.png';
|
||||
@ -73,20 +76,28 @@ class Assets {
|
||||
static const String sideBBnb3Selected = 'assets/images/side_b/bnb3_selected.png';
|
||||
static const String sideBBnb3Unselected = 'assets/images/side_b/bnb3_unselected.png';
|
||||
static const String sideBBnbBg = 'assets/images/side_b/bnb_bg.png';
|
||||
static const String sideBBottomSheetIndicator = 'assets/images/side_b/bottom_sheet_indicator.png';
|
||||
static const String sideBCollectionAlbum = 'assets/images/side_b/collection_album.png';
|
||||
static const String sideBCrossCircle = 'assets/images/side_b/cross_circle.png';
|
||||
static const String sideBDownload = 'assets/images/side_b/download.png';
|
||||
static const String sideBHomeBg = 'assets/images/side_b/home_bg.png';
|
||||
static const String sideBImgError = 'assets/images/side_b/img_error.png';
|
||||
static const String sideBImgPlaceholder = 'assets/images/side_b/img_placeholder.png';
|
||||
static const String sideBItemPlayer1 = 'assets/images/side_b/item_player1.png';
|
||||
static const String sideBLaunchImage = 'assets/images/side_b/launch_image.png';
|
||||
static const String sideBLineMenu = 'assets/images/side_b/line_menu.png';
|
||||
static const String sideBListLoop = 'assets/images/side_b/list_loop.png';
|
||||
static const String sideBLove = 'assets/images/side_b/love.png';
|
||||
static const String sideBNextSong = 'assets/images/side_b/next_song.png';
|
||||
static const String sideBLoveSolid = 'assets/images/side_b/love_solid.png';
|
||||
static const String sideBNextTrack = 'assets/images/side_b/next_track.png';
|
||||
static const String sideBNotCollectionAlbum = 'assets/images/side_b/not_collection_album.png';
|
||||
static const String sideBOfflineDownload = 'assets/images/side_b/offline_download.png';
|
||||
static const String sideBPausePlay = 'assets/images/side_b/pause_play.png';
|
||||
static const String sideBPersonalMusicLibraryBg = 'assets/images/side_b/personal_music_library_bg.png';
|
||||
static const String sideBPlaceholderLibrary = 'assets/images/side_b/placeholder_library.png';
|
||||
static const String sideBPlayList = 'assets/images/side_b/play_list.png';
|
||||
static const String sideBPreviousSong = 'assets/images/side_b/previous_song.png';
|
||||
static const String sideBPlayListDelete = 'assets/images/side_b/play_list_delete.png';
|
||||
static const String sideBPlaylistsAdd = 'assets/images/side_b/playlists_add.png';
|
||||
static const String sideBPreviousTrack = 'assets/images/side_b/previous_track.png';
|
||||
static const String sideBShufflePlayback = 'assets/images/side_b/shuffle_playback.png';
|
||||
static const String sideBSingleCycle = 'assets/images/side_b/single_cycle.png';
|
||||
static const String sideBStartPlay = 'assets/images/side_b/start_play.png';
|
||||
|
||||
@ -5,8 +5,10 @@
|
||||
import 'package:tone_snap/data/enum/app_side_enum.dart';
|
||||
|
||||
class AppConfig {
|
||||
static const appName = 'ToneSnap';
|
||||
|
||||
/// 当前App展示的一面
|
||||
static const AppSideEnum appSideEnum = AppSideEnum.sideA;
|
||||
static const AppSideEnum appSideEnum = AppSideEnum.sideB;
|
||||
|
||||
/// 默认语言环境和所在区域
|
||||
static String defaultLocale = 'zh-CN';
|
||||
51
lib/global/app_tracking_transparency_manager.dart
Normal file
@ -0,0 +1,51 @@
|
||||
// Author: fengshengxiong
|
||||
// Date: 2024/6/26
|
||||
// Description: 应用程序跟踪透明度管理器
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
|
||||
import 'package:tone_snap/utils/log_util.dart';
|
||||
|
||||
class AppTrackingTransparencyManager {
|
||||
AppTrackingTransparencyManager._();
|
||||
|
||||
static final AppTrackingTransparencyManager _instance = AppTrackingTransparencyManager._();
|
||||
|
||||
factory AppTrackingTransparencyManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
Timer? _timer;
|
||||
|
||||
/// 请求跟踪授权
|
||||
Future<void> requestATT() async {
|
||||
if (Platform.isIOS) {
|
||||
final TrackingStatus status = await AppTrackingTransparency.trackingAuthorizationStatus;
|
||||
if (status == TrackingStatus.notDetermined) {
|
||||
if (_timer != null && _timer!.isActive) {
|
||||
final TrackingStatus status = await AppTrackingTransparency.requestTrackingAuthorization();
|
||||
LogUtil.d('跟踪授权状态: $status');
|
||||
} else {
|
||||
_startTimer();
|
||||
}
|
||||
} else {
|
||||
_stopTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始定时器
|
||||
void _startTimer() {
|
||||
_timer = Timer.periodic(const Duration(seconds: 3), (Timer t) {
|
||||
requestATT();
|
||||
});
|
||||
}
|
||||
|
||||
/// 停止定时器
|
||||
void _stopTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
132
lib/main.dart
@ -10,13 +10,15 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||
import 'package:tone_snap/components/base_easyloading.dart';
|
||||
import 'package:tone_snap/data/app_config.dart';
|
||||
import 'package:tone_snap/components/music_bar.dart';
|
||||
import 'package:tone_snap/components/music_bar/music_bar_controller.dart';
|
||||
import 'package:tone_snap/data/enum/app_side_enum.dart';
|
||||
import 'package:tone_snap/data/storage/hive_storage.dart';
|
||||
import 'package:tone_snap/firebase/firebase_options.dart';
|
||||
import 'package:tone_snap/global/app_config.dart';
|
||||
import 'package:tone_snap/global/network_connectivity_service.dart';
|
||||
import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart';
|
||||
import 'package:tone_snap/res/themes/app_themes.dart';
|
||||
import 'package:tone_snap/res/values/strings.dart';
|
||||
import 'package:tone_snap/routes/app_pages.dart';
|
||||
import 'package:tone_snap/routes/app_routes.dart';
|
||||
import 'package:tone_snap/utils/file_util.dart';
|
||||
@ -26,35 +28,37 @@ import 'package:tone_snap/utils/log_util.dart';
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// 初始化Firebase
|
||||
try {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
} catch (e) {
|
||||
LogUtil.e("Firebase initialization error: $e");
|
||||
if (Platform.isIOS) {
|
||||
// 初始化Firebase
|
||||
try {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
} catch (e) {
|
||||
LogUtil.e("Firebase initialization error: $e");
|
||||
}
|
||||
|
||||
// 非异步错误
|
||||
FlutterError.onError = (errorDetails) {
|
||||
FirebaseCrashlytics.instance.recordFlutterError(errorDetails);
|
||||
};
|
||||
|
||||
// 异步错误
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: false);
|
||||
return true;
|
||||
};
|
||||
|
||||
// 初始化广告 SDK
|
||||
MobileAds.instance.initialize();
|
||||
|
||||
// 监听网络变化
|
||||
await Get.putAsync(() async => NetworkConnectivityService());
|
||||
}
|
||||
|
||||
// 非异步错误
|
||||
FlutterError.onError = (errorDetails) {
|
||||
FirebaseCrashlytics.instance.recordFlutterError(errorDetails);
|
||||
};
|
||||
|
||||
// 异步错误
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: false);
|
||||
return true;
|
||||
};
|
||||
|
||||
// 初始化广告 SDK
|
||||
MobileAds.instance.initialize();
|
||||
|
||||
// 初始化Hive
|
||||
await initHive();
|
||||
|
||||
// 监听网络变化
|
||||
await Get.putAsync(() async => NetworkConnectivityService());
|
||||
|
||||
runApp(const MyApp());
|
||||
|
||||
// 竖屏
|
||||
@ -83,44 +87,54 @@ class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final easyLoading = EasyLoading.init();
|
||||
ThemeData appTheme;
|
||||
List<NavigatorObserver> navigatorObservers = const <NavigatorObserver>[];
|
||||
if (AppConfig.appSideEnum == AppSideEnum.sideA) {
|
||||
appTheme = sideATheme;
|
||||
} else {
|
||||
appTheme = sideBTheme;
|
||||
navigatorObservers = [
|
||||
GetObserver((_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (Get.currentRoute == AppRoutes.playPage) {
|
||||
MusicBar().hide();
|
||||
} else {
|
||||
if (Get.isRegistered<MusicPlayerController>() && MusicPlayerController.to.musicModel.value.videoId != null) {
|
||||
MusicBar().show();
|
||||
}
|
||||
}
|
||||
if (Get.isRegistered<MusicBarController>()) {
|
||||
if (Get.currentRoute == AppRoutes.initialB) {
|
||||
MusicBarController.to.riseUp();
|
||||
} else {
|
||||
MusicBarController.to.toBottom();
|
||||
}
|
||||
}
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
return ScreenUtilInit(
|
||||
// 以设计稿的尺寸为基准进行适配
|
||||
designSize: const Size(375, 812),
|
||||
minTextAdapt: true,
|
||||
builder: (context, child) {
|
||||
if (AppConfig.appSideEnum == AppSideEnum.sideA) {
|
||||
return GetMaterialApp(
|
||||
title: appName,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: sideATheme,
|
||||
darkTheme: sideATheme,
|
||||
themeMode: ThemeMode.dark,
|
||||
initialRoute: AppRoutes.splashA,
|
||||
getPages: AppPages.routes,
|
||||
builder: (context, widget) {
|
||||
BaseEasyLoading.initGlobalConfig();
|
||||
widget = easyLoading(context, widget);
|
||||
// 设置文字大小不随系统设置改变
|
||||
return MediaQuery.withNoTextScaling(child: widget);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return GetMaterialApp(
|
||||
title: appName,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: sideBTheme,
|
||||
darkTheme: sideBTheme,
|
||||
themeMode: ThemeMode.dark,
|
||||
initialRoute: AppRoutes.splashB,
|
||||
getPages: AppPages.routes,
|
||||
builder: (context, widget) {
|
||||
BaseEasyLoading.initGlobalConfig();
|
||||
widget = easyLoading(context, widget);
|
||||
// 设置文字大小不随系统设置改变
|
||||
return MediaQuery.withNoTextScaling(child: widget);
|
||||
},
|
||||
);
|
||||
}
|
||||
return GetMaterialApp(
|
||||
title: AppConfig.appName,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: appTheme,
|
||||
darkTheme: appTheme,
|
||||
themeMode: ThemeMode.dark,
|
||||
initialRoute: AppRoutes.splash,
|
||||
getPages: AppPages.routes,
|
||||
navigatorObservers: navigatorObservers,
|
||||
builder: (context, widget) {
|
||||
BaseEasyLoading.configLoading();
|
||||
widget = easyLoading(context, widget);
|
||||
// 设置文字大小不随系统设置改变
|
||||
return MediaQuery.withNoTextScaling(child: widget);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/base_appbar.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/global/app_config.dart';
|
||||
import 'package:tone_snap/modules/sidea/about/about_controller.dart';
|
||||
import 'package:tone_snap/res/values/strings.dart';
|
||||
|
||||
class AboutView extends StatelessWidget {
|
||||
AboutView({super.key});
|
||||
@ -32,7 +32,7 @@ class AboutView extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: appName,
|
||||
text: AppConfig.appName,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22.sp,
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ffmpeg_kit_flutter_audio/ffmpeg_kit.dart';
|
||||
import 'package:ffmpeg_kit_flutter_audio/ffmpeg_session.dart';
|
||||
import 'package:ffmpeg_kit_flutter_audio/return_code.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/ads/interstitial_ad_manager.dart';
|
||||
import 'package:tone_snap/components/base_easyloading.dart';
|
||||
import 'package:tone_snap/data/sidea/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/sidea/storage/my_voice_data.dart';
|
||||
import 'package:tone_snap/data/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/storage/my_voice_box.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/modules/sidea/controllers/player_controller.dart';
|
||||
import 'package:tone_snap/modules/sidea/initial/initial_controller.dart';
|
||||
@ -46,7 +48,9 @@ class ChangeVoiceController extends GetxController {
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
InterstitialAdManager().loadAd();
|
||||
if (Platform.isIOS) {
|
||||
InterstitialAdManager().loadAd();
|
||||
}
|
||||
|
||||
filePath = Get.arguments;
|
||||
// playerController.setFilePath(filePath);
|
||||
@ -104,62 +108,121 @@ class ChangeVoiceController extends GetxController {
|
||||
|
||||
/// 保存
|
||||
Future<void> save() async {
|
||||
// 显示插页广告
|
||||
InterstitialAdManager().showAdIfAvailable();
|
||||
if (Platform.isIOS) {
|
||||
// 显示插页广告
|
||||
InterstitialAdManager().showAdIfAvailable(
|
||||
onTap: () async {
|
||||
// 停止播放
|
||||
if (playerController.isPlaying.value) playerController.stopPlay();
|
||||
BaseEasyLoading.loading();
|
||||
|
||||
// 停止播放
|
||||
if (playerController.isPlaying.value) playerController.stopPlay();
|
||||
BaseEasyLoading.loading();
|
||||
try {
|
||||
// 若是assets路径,转换为文件路径
|
||||
if (filePath.contains('assets')) {
|
||||
filePath = await FileUtil.getAssetsToFilePath(filePath);
|
||||
}
|
||||
|
||||
try {
|
||||
// 若是assets路径,转换为文件路径
|
||||
if (filePath.contains('assets')) {
|
||||
filePath = await FileUtil.getAssetsToFilePath(filePath);
|
||||
}
|
||||
// 输出目录
|
||||
final outputDir = await LocalPathUtil.getVoiceChangeOutputDir();
|
||||
String fileName = '${DateUtil.getNowTimeStr().replaceAll(' ', '_')}_output.mp3';
|
||||
String outputPath = '${outputDir.path}/$fileName';
|
||||
|
||||
// 输出目录
|
||||
final outputDir = await LocalPathUtil.getVoiceChangeOutputDir();
|
||||
String fileName = '${DateUtil.getNowTimeStr().replaceAll(' ', '_')}_output.mp3';
|
||||
String outputPath = '${outputDir.path}/$fileName';
|
||||
var filter = "";
|
||||
// var timber = timberList.firstWhereOrNull((e) => e.check);
|
||||
// if (timber != null) {
|
||||
// int index = timberList.indexOf(timber);
|
||||
// if (index == 5) filter = ",afftdn=nf=-30";
|
||||
// }
|
||||
|
||||
var filter = "";
|
||||
// var timber = timberList.firstWhereOrNull((e) => e.check);
|
||||
// if (timber != null) {
|
||||
// int index = timberList.indexOf(timber);
|
||||
// if (index == 5) filter = ",afftdn=nf=-30";
|
||||
// }
|
||||
if (currentIndex.value == 5) filter = ",afftdn=nf=-30,aecho=0.8:0.88:60:0.4";
|
||||
|
||||
if (currentIndex.value == 5) filter = ",afftdn=nf=-30,aecho=0.8:0.88:60:0.4";
|
||||
// 获取原始采样率
|
||||
String sampleRate = await _getSampleRate() ?? '24000';
|
||||
|
||||
// 获取原始采样率
|
||||
String sampleRate = await _getSampleRate() ?? '24000';
|
||||
// 构建 FFmpeg 命令
|
||||
final String command = '-i $filePath -af "asetrate=$sampleRate*${toneValue.value},atempo=${soundSpeedValue.value}$filter" $outputPath';
|
||||
|
||||
// 构建 FFmpeg 命令
|
||||
final String command = '-i $filePath -af "asetrate=$sampleRate*${toneValue.value},atempo=${soundSpeedValue.value}$filter" $outputPath';
|
||||
// 执行 FFmpeg 命令
|
||||
FFmpegSession session = await FFmpegKit.execute(command);
|
||||
|
||||
// 执行 FFmpeg 命令
|
||||
FFmpegSession session = await FFmpegKit.execute(command);
|
||||
// 获取执行结果
|
||||
final returnCode = await session.getReturnCode();
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
LogUtil.d('Audio processing successful');
|
||||
try {
|
||||
await MyVoiceBox().addData(VoiceModel(name: fileName, path: outputPath));
|
||||
BaseEasyLoading.toast('Save successful');
|
||||
|
||||
// 获取执行结果
|
||||
final returnCode = await session.getReturnCode();
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
LogUtil.d('Audio processing successful');
|
||||
try {
|
||||
await MyVoiceData().addData(VoiceModel(name: fileName, path: outputPath));
|
||||
BaseEasyLoading.toast('Save successful');
|
||||
// 回到首页-我的页面
|
||||
Get.until((route) => route.settings.name == AppRoutes.initialA);
|
||||
InitialController.to.onBottomAppBarItemChanged(2);
|
||||
} catch (e) {
|
||||
BaseEasyLoading.toast('Save failed');
|
||||
}
|
||||
} else {
|
||||
LogUtil.d('Audio processing failed');
|
||||
BaseEasyLoading.toast('Audio processing failed');
|
||||
}
|
||||
} catch (e) {
|
||||
BaseEasyLoading.toast('Audio processing failed');
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// 停止播放
|
||||
if (playerController.isPlaying.value) playerController.stopPlay();
|
||||
BaseEasyLoading.loading();
|
||||
|
||||
// 回到首页-我的页面
|
||||
Get.until((route) => route.settings.name == AppRoutes.initialA);
|
||||
InitialController.to.onBottomAppBarItemChanged(2);
|
||||
} catch (e) {
|
||||
BaseEasyLoading.toast('Save failed');
|
||||
try {
|
||||
// 若是assets路径,转换为文件路径
|
||||
if (filePath.contains('assets')) {
|
||||
filePath = await FileUtil.getAssetsToFilePath(filePath);
|
||||
}
|
||||
} else {
|
||||
LogUtil.d('Audio processing failed');
|
||||
|
||||
// 输出目录
|
||||
final outputDir = await LocalPathUtil.getVoiceChangeOutputDir();
|
||||
String fileName = '${DateUtil.getNowTimeStr().replaceAll(' ', '_')}_output.mp3';
|
||||
String outputPath = '${outputDir.path}/$fileName';
|
||||
|
||||
var filter = "";
|
||||
// var timber = timberList.firstWhereOrNull((e) => e.check);
|
||||
// if (timber != null) {
|
||||
// int index = timberList.indexOf(timber);
|
||||
// if (index == 5) filter = ",afftdn=nf=-30";
|
||||
// }
|
||||
|
||||
if (currentIndex.value == 5) filter = ",afftdn=nf=-30,aecho=0.8:0.88:60:0.4";
|
||||
|
||||
// 获取原始采样率
|
||||
String sampleRate = await _getSampleRate() ?? '24000';
|
||||
|
||||
// 构建 FFmpeg 命令
|
||||
final String command = '-i $filePath -af "asetrate=$sampleRate*${toneValue.value},atempo=${soundSpeedValue.value}$filter" $outputPath';
|
||||
|
||||
// 执行 FFmpeg 命令
|
||||
FFmpegSession session = await FFmpegKit.execute(command);
|
||||
|
||||
// 获取执行结果
|
||||
final returnCode = await session.getReturnCode();
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
LogUtil.d('Audio processing successful');
|
||||
try {
|
||||
await MyVoiceBox().addData(VoiceModel(name: fileName, path: outputPath));
|
||||
BaseEasyLoading.toast('Save successful');
|
||||
|
||||
// 回到首页-我的页面
|
||||
Get.until((route) => route.settings.name == AppRoutes.initialA);
|
||||
InitialController.to.onBottomAppBarItemChanged(2);
|
||||
} catch (e) {
|
||||
BaseEasyLoading.toast('Save failed');
|
||||
}
|
||||
} else {
|
||||
LogUtil.d('Audio processing failed');
|
||||
BaseEasyLoading.toast('Audio processing failed');
|
||||
}
|
||||
} catch (e) {
|
||||
BaseEasyLoading.toast('Audio processing failed');
|
||||
}
|
||||
} catch (e) {
|
||||
BaseEasyLoading.toast('Audio processing failed');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,8 +4,8 @@ import 'package:tone_snap/components/base_easyloading.dart';
|
||||
import 'package:tone_snap/components/dialog/remind_dialog.dart';
|
||||
import 'package:tone_snap/components/dialog/rename_dialog.dart';
|
||||
import 'package:tone_snap/components/view_state_widget.dart';
|
||||
import 'package:tone_snap/data/sidea/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/sidea/storage/favorite_data.dart';
|
||||
import 'package:tone_snap/data/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/storage/favorite_box.dart';
|
||||
import 'package:tone_snap/modules/sidea/initial/initial_controller.dart';
|
||||
import 'package:tone_snap/routes/app_routes.dart';
|
||||
|
||||
@ -22,7 +22,7 @@ class FavouriteController extends GetxController {
|
||||
}
|
||||
|
||||
void getData() {
|
||||
voiceList.value = FavoriteData().getList().reversed.toList();
|
||||
voiceList.value = FavoriteBox().getList().reversed.toList();
|
||||
_refreshList();
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/base_easyloading.dart';
|
||||
import 'package:tone_snap/data/sidea/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/models/voice_model.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/modules/sidea/controllers/player_controller.dart';
|
||||
import 'package:tone_snap/modules/sidea/initial/initial_controller.dart';
|
||||
|
||||
@ -4,7 +4,7 @@ import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/circular_notch_clipper.dart';
|
||||
import 'package:tone_snap/components/my_marquee_text.dart';
|
||||
import 'package:tone_snap/modules/sidea/widgets/head_label.dart';
|
||||
import 'package:tone_snap/data/sidea/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/models/voice_model.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/modules/sidea/home/home_controller.dart';
|
||||
import 'package:tone_snap/utils/obj_util.dart';
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/global/app_lifecycle_reactor.dart';
|
||||
import 'package:tone_snap/ads/app_open_ad_manager.dart';
|
||||
import 'package:tone_snap/data/sidea/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/sidea/storage/favorite_data.dart';
|
||||
import 'package:tone_snap/data/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/storage/favorite_box.dart';
|
||||
import 'package:tone_snap/firebase/firebase_analytics_manager.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/global/app_lifecycle_reactor.dart';
|
||||
import 'package:tone_snap/modules/sidea/controllers/player_controller.dart';
|
||||
import 'package:tone_snap/modules/sidea/favourite/favourite_controller.dart';
|
||||
import 'package:tone_snap/modules/sidea/home/home_view.dart';
|
||||
@ -14,8 +15,6 @@ import 'package:tone_snap/modules/sidea/my_voice/my_voice_controller.dart';
|
||||
import 'package:tone_snap/modules/sidea/settings/settings_view.dart';
|
||||
import 'package:tone_snap/routes/app_routes.dart';
|
||||
|
||||
import '../../../utils/tracking_authorization_util.dart';
|
||||
|
||||
class InitialController extends GetxController {
|
||||
static InitialController get to => Get.find<InitialController>();
|
||||
var playerController = PlayerController.to;
|
||||
@ -37,11 +36,8 @@ class InitialController extends GetxController {
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// ATT授权
|
||||
TrackingAuthorizationUtil.requestTrackingAuthorization();
|
||||
|
||||
_appLifecycleReactor = AppLifecycleReactor();
|
||||
_appLifecycleReactor.listenToAppStateChanges();
|
||||
// _appLifecycleReactor = AppLifecycleReactor();
|
||||
// _appLifecycleReactor.listenToAppStateChanges();
|
||||
|
||||
pageController = PageController(initialPage: currentIndex.value);
|
||||
}
|
||||
@ -49,8 +45,8 @@ class InitialController extends GetxController {
|
||||
@override
|
||||
void onReady() async {
|
||||
super.onReady();
|
||||
await FirebaseAnalyticsManager.setCrashlyticsCollectionEnabled();
|
||||
_addEventLog();
|
||||
// await FirebaseAnalyticsManager.setCrashlyticsCollectionEnabled();
|
||||
// _addEventLog();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -64,9 +60,9 @@ class InitialController extends GetxController {
|
||||
await PlayerController.to.stopPlay();
|
||||
Get.toNamed(AppRoutes.uploadMethod);
|
||||
} else {
|
||||
if (index == 0) {
|
||||
_addEventLog();
|
||||
}
|
||||
// if (index == 0) {
|
||||
// _addEventLog();
|
||||
// }
|
||||
if (index == 2) _refreshMe();
|
||||
currentIndex.value = index;
|
||||
pageController.jumpToPage(index);
|
||||
@ -84,7 +80,7 @@ class InitialController extends GetxController {
|
||||
}
|
||||
|
||||
VoiceModel? getIsFavouriteModel() {
|
||||
final list = FavoriteData().getList();
|
||||
final list = FavoriteBox().getList();
|
||||
return list.firstWhereOrNull((e) => e.path == currentPlayVoiceModel.value?.path);
|
||||
}
|
||||
|
||||
@ -94,7 +90,7 @@ class InitialController extends GetxController {
|
||||
isFavourite.value = false;
|
||||
} else {
|
||||
if (currentPlayVoiceModel.value != null) {
|
||||
await FavoriteData().addData(currentPlayVoiceModel.value!.copyWith());
|
||||
await FavoriteBox().addData(currentPlayVoiceModel.value!.copyWith());
|
||||
isFavourite.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import 'package:get/get.dart';
|
||||
import 'package:tone_snap/modules/sidea/favourite/favourite_view.dart';
|
||||
import 'package:tone_snap/modules/sidea/my_voice/my_voice_view.dart';
|
||||
|
||||
class MeController extends GetxController with GetTickerProviderStateMixin {
|
||||
class MeController extends GetxController with GetSingleTickerProviderStateMixin {
|
||||
late TabController tabController;
|
||||
final labels = ['My Voice', 'Favourite'];
|
||||
final pages = [const MyVoiceView(), const FavouriteView()];
|
||||
|
||||
@ -4,8 +4,8 @@ import 'package:tone_snap/components/base_easyloading.dart';
|
||||
import 'package:tone_snap/components/dialog/remind_dialog.dart';
|
||||
import 'package:tone_snap/components/dialog/rename_dialog.dart';
|
||||
import 'package:tone_snap/components/view_state_widget.dart';
|
||||
import 'package:tone_snap/data/sidea/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/sidea/storage/my_voice_data.dart';
|
||||
import 'package:tone_snap/data/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/storage/my_voice_box.dart';
|
||||
import 'package:tone_snap/modules/sidea/initial/initial_controller.dart';
|
||||
import 'package:tone_snap/routes/app_routes.dart';
|
||||
|
||||
@ -22,7 +22,7 @@ class MyVoiceController extends GetxController {
|
||||
}
|
||||
|
||||
void getData() {
|
||||
voiceList.value = MyVoiceData().getList().reversed.toList();
|
||||
voiceList.value = MyVoiceBox().getList().reversed.toList();
|
||||
_refreshList();
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/data/sidea/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/models/voice_model.dart';
|
||||
import 'package:tone_snap/modules/sidea/controllers/player_controller.dart';
|
||||
import 'package:tone_snap/modules/sidea/initial/initial_controller.dart';
|
||||
import 'package:tone_snap/routes/app_routes.dart';
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/ads/app_open_ad_manager.dart';
|
||||
import 'package:tone_snap/utils/tracking_authorization_util.dart';
|
||||
|
||||
class SplashController extends GetxController with GetSingleTickerProviderStateMixin {
|
||||
var processValue = 0.0.obs;
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 10),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// 创建 Tween 并绑定到 AnimationController
|
||||
_animation = Tween<double>(begin: 0, end: 1).animate(_controller)
|
||||
..addListener(() {
|
||||
processValue.value = _animation.value;
|
||||
if (processValue.value >= 1) {
|
||||
// 显示开屏广告
|
||||
AppOpenAdManager().showAdIfAvailable();
|
||||
}
|
||||
});
|
||||
|
||||
// 启动动画
|
||||
_controller.forward();
|
||||
|
||||
// 延迟3秒执行
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
// ATT授权
|
||||
TrackingAuthorizationUtil.requestTrackingAuthorization();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_controller.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:tone_snap/components/my_marquee_text.dart';
|
||||
import 'package:tone_snap/data/sidea/models/voice_model.dart';
|
||||
import 'package:tone_snap/data/models/voice_model.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/utils/obj_util.dart';
|
||||
|
||||
|
||||
9
lib/modules/sideb/album/album_binding.dart
Normal file
@ -0,0 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/modules/sideb/album/album_controller.dart';
|
||||
|
||||
class AlbumBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => AlbumController());
|
||||
}
|
||||
}
|
||||
104
lib/modules/sideb/album/album_controller.dart
Normal file
@ -0,0 +1,104 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/data/api/music_api.dart';
|
||||
import 'package:tone_snap/data/models/browse_album_model.dart';
|
||||
import 'package:tone_snap/data/models/music_model.dart';
|
||||
import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart';
|
||||
import 'package:tone_snap/routes/app_routes.dart';
|
||||
import 'package:tone_snap/utils/obj_util.dart';
|
||||
|
||||
class AlbumController extends GetxController {
|
||||
var musicPlayerController = MusicPlayerController.to;
|
||||
String? browseId;
|
||||
String? params;
|
||||
|
||||
/// 背景图、标题 、描述
|
||||
var bgThumbnail = Rx<String?>(null);
|
||||
var title = Rx<String?>(null);
|
||||
var description = Rx<String?>(null);
|
||||
|
||||
/// 专辑列表
|
||||
var albumList = <MusicModel>[].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
browseId = Get.arguments['browseId'];
|
||||
params = Get.arguments['params'];
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
_requestAlbum();
|
||||
}
|
||||
|
||||
/// 请求专辑
|
||||
Future<void> _requestAlbum() async {
|
||||
Map<String, dynamic> queryParameters = {
|
||||
'browseId': browseId,
|
||||
'params': params,
|
||||
'prettyPrint': false
|
||||
};
|
||||
BrowseAlbumModel? browseAlbumModel = await MusicApi.browse<BrowseAlbumModel>(queryParameters: queryParameters, formJson: BrowseAlbumModel.fromMap);
|
||||
if (browseAlbumModel != null) {
|
||||
var thumbnails = browseAlbumModel.background?.musicThumbnailRenderer?.thumbnail?.thumbnails;
|
||||
if (thumbnails != null && thumbnails.isNotEmpty) {
|
||||
bgThumbnail.value = thumbnails.last.url;
|
||||
}
|
||||
var tabs = browseAlbumModel.contents?.twoColumnBrowseResultsRenderer?.tabs;
|
||||
if (tabs != null && tabs.isNotEmpty) {
|
||||
var contents = tabs[0].tabRenderer?.content?.sectionListRenderer?.contents;
|
||||
if (contents != null && contents.isNotEmpty) {
|
||||
var runs = contents[0].musicResponsiveHeaderRenderer?.title?.runs;
|
||||
if (runs != null && runs.isNotEmpty) {
|
||||
title.value = runs[0].text;
|
||||
}
|
||||
var runs1 = contents[0].musicResponsiveHeaderRenderer?.description?.musicDescriptionShelfRenderer?.description?.runs;
|
||||
if (runs1 != null && runs1.isNotEmpty) {
|
||||
description.value = runs1.map((e) => e.text).join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var contents = browseAlbumModel.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer?.contents;
|
||||
if (contents != null && contents.isNotEmpty) {
|
||||
var contents1 = contents[0].musicShelfRenderer?.contents;
|
||||
if (contents1 != null && contents1.isNotEmpty) {
|
||||
for (var o in contents1) {
|
||||
var playListModel = MusicModel();
|
||||
var flexColumns = o.musicResponsiveListItemRenderer?.flexColumns;
|
||||
if (flexColumns != null && flexColumns.isNotEmpty) {
|
||||
var runs = flexColumns[0].musicResponsiveListItemFlexColumnRenderer?.text?.runs;
|
||||
if (runs != null && runs.isNotEmpty) {
|
||||
playListModel.title = runs[0].text;
|
||||
}
|
||||
for (var o in flexColumns) {
|
||||
var runs = o.musicResponsiveListItemFlexColumnRenderer?.text?.runs;
|
||||
if (runs != null && runs.isNotEmpty) {
|
||||
playListModel.subTitle = runs.map((e) => e.text).join();
|
||||
}
|
||||
}
|
||||
}
|
||||
var fixedColumns = o.musicResponsiveListItemRenderer?.fixedColumns;
|
||||
if (fixedColumns != null && fixedColumns.isNotEmpty) {
|
||||
var runs = fixedColumns[0].musicResponsiveListItemFixedColumnRenderer?.text?.runs;
|
||||
if (runs != null && runs.isNotEmpty) {
|
||||
playListModel.subTitle = '${ObjUtil.getStr(playListModel.subTitle)} • ${runs.map((e) => e.text).join()}';
|
||||
}
|
||||
}
|
||||
var playlistItemData = o.musicResponsiveListItemRenderer?.playlistItemData;
|
||||
if (playlistItemData != null) {
|
||||
playListModel.videoId = playlistItemData.videoId;
|
||||
}
|
||||
albumList.add(playListModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 点击专辑列表歌曲
|
||||
void onTapAlbumItem(int index, MusicModel model) {
|
||||
Get.toNamed(AppRoutes.playPage, arguments: {'playList': albumList, 'videoId': model.videoId});
|
||||
}
|
||||
}
|
||||
229
lib/modules/sideb/album/album_view.dart
Normal file
@ -0,0 +1,229 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/base_scrollbar.dart';
|
||||
import 'package:tone_snap/components/music_bar.dart';
|
||||
import 'package:tone_snap/components/my_marquee_text.dart';
|
||||
import 'package:tone_snap/components/network_image_widget.dart';
|
||||
import 'package:tone_snap/data/models/music_model.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/modules/sideb/album/album_controller.dart';
|
||||
import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart';
|
||||
import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart';
|
||||
import 'package:tone_snap/res/themes/app_colors.dart';
|
||||
import 'package:tone_snap/res/themes/app_sizes.dart';
|
||||
import 'package:tone_snap/utils/obj_util.dart';
|
||||
|
||||
class AlbumView extends StatelessWidget {
|
||||
AlbumView({super.key});
|
||||
|
||||
final controller = Get.find<AlbumController>();
|
||||
final musicPlayerController = MusicPlayerController.to;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Obx(() {
|
||||
return NetworkImageWidget(
|
||||
url: controller.bgThumbnail.value,
|
||||
width: 1.sw,
|
||||
height: 413.h,
|
||||
noPlaceholder: true,
|
||||
);
|
||||
}),
|
||||
Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Column(
|
||||
children: [
|
||||
const MusicAppbar(),
|
||||
SizedBox(height: 152.h),
|
||||
_buildIntroduction(),
|
||||
_buildList(),
|
||||
Obx(() {
|
||||
return Visibility(
|
||||
visible: MusicBar().isShow.value,
|
||||
child: SizedBox(height: paddingBottomMusicBarHeight(context)),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIntroduction() {
|
||||
return Stack(
|
||||
children: [
|
||||
ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(topLeft: Radius.circular(16.r), topRight: Radius.circular(16.r)),
|
||||
child: Image.asset(
|
||||
Assets.sideBAlbumTitleBg,
|
||||
width: 1.sw,
|
||||
height: 173.h,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18).w,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 18.h),
|
||||
Obx(() {
|
||||
return Text(
|
||||
ObjUtil.getStr(controller.title.value),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 12.h),
|
||||
Obx(() {
|
||||
return Text(
|
||||
ObjUtil.getStr(controller.description.value),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0x99FFFFFF),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 24.h),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
width: 138.w,
|
||||
height: 32.h,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4).w,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16).r,
|
||||
color: const Color(0x1A80F988),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
Assets.sideBAlbumTotal,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
return Text(
|
||||
'Play all (${controller.albumList.length})',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Image.asset(
|
||||
Assets.sideBNotCollectionAlbum,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildList() {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
color: const Color(0xFF121212),
|
||||
child: BaseScrollbar(
|
||||
child: Obx(() {
|
||||
return ListView.builder(
|
||||
itemCount: controller.albumList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildListItem(index, controller.albumList[index]);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListItem(int index, MusicModel model) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => controller.onTapAlbumItem(index, model),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8).h,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 18.w),
|
||||
Text(
|
||||
(index + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(() {
|
||||
return MyMarqueeText(
|
||||
enable: musicPlayerController.musicModel.value.videoId == model.videoId,
|
||||
text: ObjUtil.getStr(model.title),
|
||||
textStyle: TextStyle(
|
||||
color: musicPlayerController.musicModel.value.videoId == model.videoId
|
||||
? seedColor
|
||||
: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 8.h),
|
||||
Obx(() {
|
||||
return MyMarqueeText(
|
||||
enable: musicPlayerController.musicModel.value.videoId == model.videoId,
|
||||
text: ObjUtil.getStr(model.subTitle),
|
||||
textStyle: TextStyle(
|
||||
color: musicPlayerController.musicModel.value.videoId == model.videoId
|
||||
? seedColor
|
||||
: const Color(0x99FFFFFF),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'collect_playlists_controller.dart';
|
||||
|
||||
class CollectPlaylistsBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => CollectPlaylistsController());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class CollectPlaylistsController extends GetxController {
|
||||
ScrollController scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
scrollController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/base_scrollbar.dart';
|
||||
import 'package:tone_snap/components/network_image_widget.dart';
|
||||
|
||||
import 'collect_playlists_controller.dart';
|
||||
|
||||
class CollectPlaylistsView extends GetView<CollectPlaylistsController> {
|
||||
const CollectPlaylistsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.find<CollectPlaylistsController>();
|
||||
|
||||
return BaseScrollbar(
|
||||
scrollController: controller.scrollController,
|
||||
child: ListView.builder(
|
||||
controller: controller.scrollController,
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildItem();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem() {
|
||||
return InkWell(
|
||||
onTap: (){},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 16.w),
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImageWidget(
|
||||
url: '',
|
||||
width: 60.w,
|
||||
height: 60.w,
|
||||
radius: 8.r,
|
||||
),
|
||||
SizedBox(width: 14.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'There For You',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
'234 songs',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF999999),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,16 +3,22 @@
|
||||
// Description: 音乐播放器控制器
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:tone_snap/components/base_easyloading.dart';
|
||||
import 'package:tone_snap/components/music_bar.dart';
|
||||
import 'package:tone_snap/data/api/music_api.dart';
|
||||
import 'package:tone_snap/data/cache/music_cache_manager.dart';
|
||||
import 'package:tone_snap/data/enum/play_mode.dart';
|
||||
import 'package:tone_snap/data/models/music_model.dart';
|
||||
import 'package:tone_snap/data/models/player_model.dart';
|
||||
import 'package:tone_snap/data/storage/music_box.dart';
|
||||
import 'package:tone_snap/routes/app_routes.dart';
|
||||
import 'package:tone_snap/utils/audio_util.dart';
|
||||
import 'package:tone_snap/utils/date_util.dart';
|
||||
import 'package:tone_snap/utils/log_util.dart';
|
||||
import 'package:tone_snap/utils/num_util.dart';
|
||||
|
||||
class MusicPlayerController extends GetxController {
|
||||
static MusicPlayerController get to => Get.put(MusicPlayerController(), permanent: true);
|
||||
@ -22,31 +28,46 @@ class MusicPlayerController extends GetxController {
|
||||
StreamSubscription<Duration>? _positionSubscription;
|
||||
StreamSubscription<PlayerState>? _playerStateSubscription;
|
||||
|
||||
/// 缓存管理器
|
||||
// final cacheManager = DefaultCacheManager();
|
||||
final cacheManager = MusicCacheManager.instance;
|
||||
var cacheKey = '';
|
||||
|
||||
/// 是否正在从缓存播放中
|
||||
var _isPlayingFromCache = false;
|
||||
|
||||
/// 当前播放的url
|
||||
var currentUrl = '';
|
||||
|
||||
/// 是否正在播放
|
||||
var isPlaying = false.obs;
|
||||
|
||||
/// 总时长、已播放的时长、缓冲时长
|
||||
var totalDuration = Duration.zero.obs;
|
||||
var positionDuration = Duration.zero.obs;
|
||||
var bufferedDuration = Duration.zero.obs;
|
||||
|
||||
/// 记录进度条拖动前缓冲的位置
|
||||
Duration? bufferedPosition;
|
||||
/// 缓存管理器
|
||||
// final _cacheManager = DefaultCacheManager();
|
||||
final _cacheManager = MusicCacheManager.instance;
|
||||
|
||||
/// 拖动前是否是播放状态
|
||||
/// 是否正在播放
|
||||
var isPlaying = false.obs;
|
||||
|
||||
/// 播放器处理状态
|
||||
var processingState = ProcessingState.idle.obs;
|
||||
|
||||
/// _player.seek(Duration.zero) 时,这可能会导致播放状态再次变为 completed。
|
||||
/// 为了避免这种情况,在处理 completed 状态时添加一个标志位,确保在处理完成状态时不会重复触发
|
||||
/// 是否已经处理过 completed 状态
|
||||
bool _isCompletedHandled = false;
|
||||
|
||||
/// 拖动开始时是否是播放状态
|
||||
var seekFrontPlaying = true;
|
||||
|
||||
/// 播放模式,默认列表循环
|
||||
var playMode = MusicBox().getPlayMode().obs;
|
||||
var _playModeIndex = 0;
|
||||
|
||||
/// 播放列表
|
||||
var playList = <MusicModel>[].obs;
|
||||
|
||||
/// 播放历史 videoId 集合
|
||||
/// 随机播放切换到上一首时,从历史记录中取出上一个播放的 videoId
|
||||
final List<String> _playHistory = [];
|
||||
|
||||
/// 播放的歌曲模型
|
||||
var musicModel = MusicModel().obs;
|
||||
|
||||
/// 当前播放的歌曲索引
|
||||
int _currentIndex = 0;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@ -55,118 +76,96 @@ class MusicPlayerController extends GetxController {
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
MusicBar().hide();
|
||||
_cancelListening();
|
||||
_player.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// 播放新的 url
|
||||
Future<void> playNewUrl(String newUrl, String videoId) async {
|
||||
await _initializePlayer(newUrl, videoId);
|
||||
_player.play();
|
||||
/// 播放指定索引的歌曲
|
||||
void playMusicSpecifyIndex(int index) {
|
||||
_currentIndex = index;
|
||||
playMusic();
|
||||
}
|
||||
|
||||
// 使用的 music_$videoId 作为唯一的 key
|
||||
cacheKey = 'music_$videoId';
|
||||
/// 设置播放列表
|
||||
void setPlayList(String videoId, List<MusicModel> list) {
|
||||
_resetPlaybackStatus();
|
||||
playList.value = list;
|
||||
_playHistory.clear();
|
||||
|
||||
MusicModel? musicModel = playList.firstWhereOrNull((e) => e.videoId == videoId);
|
||||
int index = musicModel != null ? playList.indexOf(musicModel) : 0;
|
||||
_currentIndex = index;
|
||||
}
|
||||
|
||||
/// 播放歌曲
|
||||
Future<void> playMusic() async {
|
||||
Get.currentRoute != AppRoutes.playPage ? MusicBar().show() : MusicBar().hide();
|
||||
_resetPlaybackStatus();
|
||||
if (playList.isNotEmpty) {
|
||||
musicModel.update((e) {
|
||||
e?.thumbnail = playList[_currentIndex].thumbnail;
|
||||
e?.title = playList[_currentIndex].title;
|
||||
e?.subTitle = playList[_currentIndex].subTitle;
|
||||
});
|
||||
if (playList[_currentIndex].url == null) {
|
||||
await _getMusicUrl();
|
||||
}
|
||||
if (playList[_currentIndex].url == null) {
|
||||
BaseEasyLoading.toast('Resource not obtained');
|
||||
// nextTrack();
|
||||
return;
|
||||
}
|
||||
musicModel.value = playList[_currentIndex].copyWith();
|
||||
await _initializePlayer();
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前歌曲的播放 url
|
||||
Future<void> _getMusicUrl() async {
|
||||
PlayerModel? model = await MusicApi.player(videoId: playList[_currentIndex].videoId);
|
||||
if (model != null && model.streamingData != null) {
|
||||
var formats = model.streamingData?.formats;
|
||||
if (formats != null && playList.isNotEmpty) {
|
||||
playList[_currentIndex].url = formats[0].url;
|
||||
}
|
||||
}
|
||||
if (model != null && model.videoDetails?.thumbnail?.thumbnails != null) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置 url 和监听器
|
||||
Future<void> _initializePlayer(String url, String videoId) async {
|
||||
Future<void> _initializePlayer() async {
|
||||
try {
|
||||
currentUrl = url;
|
||||
// 检查是否有缓存
|
||||
FileInfo? fileInfo = await cacheManager.getFileFromMemory(cacheKey);
|
||||
FileInfo? fileInfo = await MusicCacheManager.checkCache(musicModel.value.videoId!);
|
||||
if (fileInfo != null) {
|
||||
LogUtil.d('有缓存,文件路径=${fileInfo.file.path}');
|
||||
LogUtil.d('读取缓存=${fileInfo.file.path}');
|
||||
|
||||
// 如果有缓存,使用缓存文件
|
||||
await _player.setFilePath(fileInfo.file.path);
|
||||
_isPlayingFromCache = true;
|
||||
} else {
|
||||
// 如果没有缓存,通过 url 加载音源并开始播放,同时启动缓存下载
|
||||
await _player.setUrl(currentUrl);
|
||||
_isPlayingFromCache = false;
|
||||
// 如果没有缓存,通过 url 加载音源并开始播放,
|
||||
await _player.setUrl(musicModel.value.url!);
|
||||
|
||||
// 下载并缓存文件
|
||||
cacheManager.downloadFile(currentUrl, key: cacheKey).then((fileInfo) {
|
||||
// 如果缓存下载完成且当前不是从缓存播放,切换到缓存文件
|
||||
if (!_isPlayingFromCache) {
|
||||
LogUtil.d('新歌曲缓存路径=${fileInfo.file.path}');
|
||||
_player.setFilePath(fileInfo.file.path);
|
||||
_isPlayingFromCache = true;
|
||||
}
|
||||
// 同时启动缓存下载
|
||||
_cacheManager.downloadFile(musicModel.value.url!, key: MusicCacheManager.getCacheKey(musicModel.value.videoId!)).then((fileInfo) {
|
||||
LogUtil.d('缓存下载成功=${fileInfo.file.path}');
|
||||
});
|
||||
}
|
||||
_player.setLoopMode(LoopMode.off);
|
||||
_cancelListening();
|
||||
_addListening();
|
||||
_player.play();
|
||||
} catch (e) {
|
||||
LogUtil.e('Error initializing player: $e');
|
||||
BaseEasyLoading.toast('Error initializing player');
|
||||
}
|
||||
}
|
||||
|
||||
/// 播放/暂停
|
||||
Future<void> playPause() async {
|
||||
if (_player.playing) {
|
||||
_player.pause();
|
||||
} else {
|
||||
if (_player.processingState == ProcessingState.idle) {
|
||||
BaseEasyLoading.loading();
|
||||
await _player.load();
|
||||
BaseEasyLoading.dismiss();
|
||||
}
|
||||
_player.play();
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止播放
|
||||
void stopPlay() {
|
||||
_player.stop();
|
||||
_player.seek(Duration.zero);
|
||||
positionDuration.value = Duration.zero;
|
||||
}
|
||||
|
||||
/// 将播放器的播放位置设置为指定的时间
|
||||
void seekToPosition(double value) {
|
||||
if (_player.processingState == ProcessingState.ready) {
|
||||
positionDuration.value = Duration(seconds: value.toInt());
|
||||
_player.seek(positionDuration.value);
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始/结束拖动进度条
|
||||
Future<void> seekStartEnd(int value) async {
|
||||
bufferedPosition = _player.bufferedPosition;
|
||||
if (_player.processingState == ProcessingState.ready) {
|
||||
if (value == 0) {
|
||||
seekFrontPlaying = _player.playing;
|
||||
if (_player.playing) {
|
||||
_player.pause();
|
||||
}
|
||||
} else {
|
||||
// 拖动前如果是播放状态,拖动完成后恢复播放,否则相反
|
||||
if (seekFrontPlaying) {
|
||||
_player.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 上一首
|
||||
void seekToPrevious() {
|
||||
_player.seekToPrevious();
|
||||
}
|
||||
|
||||
/// 下一首
|
||||
void seekToNext() {
|
||||
_player.seekToNext();
|
||||
}
|
||||
|
||||
/// 监听
|
||||
void _addListening() {
|
||||
_bufferedSubscription = _player.bufferedPositionStream.listen((bufferedPosition) {
|
||||
bufferedDuration.value = bufferedPosition;
|
||||
});
|
||||
|
||||
_durationSubscription = _player.durationStream.listen((duration) {
|
||||
totalDuration.value = duration ?? Duration.zero;
|
||||
});
|
||||
@ -175,8 +174,18 @@ class MusicPlayerController extends GetxController {
|
||||
positionDuration.value = position;
|
||||
});
|
||||
|
||||
_bufferedSubscription = _player.bufferedPositionStream.listen((bufferedPosition) {
|
||||
int comparison = bufferedPosition.compareTo(totalDuration.value);
|
||||
if (comparison > 0) {
|
||||
bufferedDuration.value = totalDuration.value;
|
||||
} else {
|
||||
bufferedDuration.value = bufferedPosition;
|
||||
}
|
||||
});
|
||||
|
||||
_playerStateSubscription = _player.playerStateStream.listen((playerState) {
|
||||
isPlaying.value = _player.playing;
|
||||
processingState.value = playerState.processingState;
|
||||
switch (playerState.processingState) {
|
||||
case ProcessingState.idle:
|
||||
break;
|
||||
@ -185,9 +194,14 @@ class MusicPlayerController extends GetxController {
|
||||
case ProcessingState.buffering:
|
||||
break;
|
||||
case ProcessingState.ready:
|
||||
_isCompletedHandled = false;
|
||||
break;
|
||||
case ProcessingState.completed:
|
||||
stopPlay();
|
||||
if (!_isCompletedHandled) {
|
||||
_isCompletedHandled = true;
|
||||
_player.seek(Duration.zero);
|
||||
nextTrack();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
@ -201,23 +215,118 @@ class MusicPlayerController extends GetxController {
|
||||
_playerStateSubscription?.cancel();
|
||||
}
|
||||
|
||||
/// 返回格式化后的总时长
|
||||
String getTotalDuration() {
|
||||
return DateUtil.playDuration(totalDuration.value);
|
||||
}
|
||||
|
||||
/// 返回格式化后的已持续时间
|
||||
String getPositionDuration() {
|
||||
return DateUtil.playDuration(positionDuration.value);
|
||||
}
|
||||
|
||||
/// 文件是否存在
|
||||
Future<bool> _fileExists(String path) async {
|
||||
if (await File(path).exists()) {
|
||||
return true;
|
||||
} else {
|
||||
BaseEasyLoading.toast('Local resource does not exist');
|
||||
return false;
|
||||
/// 将播放器的播放位置设置为指定的时间
|
||||
void seekToPosition(double value) {
|
||||
if (processingState.value == ProcessingState.ready) {
|
||||
positionDuration.value = Duration(seconds: value.toInt());
|
||||
_player.seek(positionDuration.value);
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始/结束拖动进度条
|
||||
Future<void> seekStartEnd(int value) async {
|
||||
if (processingState.value == ProcessingState.ready) {
|
||||
if (value == 0) {
|
||||
seekFrontPlaying = _player.playing;
|
||||
if (_player.playing) {
|
||||
_player.pause();
|
||||
}
|
||||
} else {
|
||||
// 拖动前如果是播放状态,拖动完成后恢复播放
|
||||
if (seekFrontPlaying) {
|
||||
_player.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 播放/暂停
|
||||
Future<void> playPause() async {
|
||||
if (_player.playing) {
|
||||
_player.pause();
|
||||
} else {
|
||||
if (processingState.value == ProcessingState.ready) {
|
||||
_player.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 上一首
|
||||
void previousTrack() {
|
||||
if (playList.isNotEmpty) {
|
||||
switch(playMode.value) {
|
||||
case PlayMode.listLoop:
|
||||
_currentIndex = (_currentIndex - 1 + playList.length) % playList.length;
|
||||
playMusic();
|
||||
break;
|
||||
case PlayMode.random:
|
||||
bool historyExist = false;
|
||||
for (var i = _playHistory.length - 1; i >= 0; --i) {
|
||||
var history = _playHistory[i];
|
||||
MusicModel? model = playList.firstWhereOrNull((e) => e.videoId == history);
|
||||
if (model != null) {
|
||||
_currentIndex = playList.indexOf(model);
|
||||
_playHistory.remove(history);
|
||||
historyExist = true;
|
||||
break;
|
||||
} else {
|
||||
_playHistory.remove(history);
|
||||
}
|
||||
}
|
||||
if (!historyExist) {
|
||||
_getRandomNumber();
|
||||
}
|
||||
playMusic();
|
||||
break;
|
||||
case PlayMode.singleCycle:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 下一首
|
||||
void nextTrack() {
|
||||
if (playList.isNotEmpty) {
|
||||
switch(playMode.value) {
|
||||
case PlayMode.listLoop:
|
||||
_currentIndex = (_currentIndex + 1) % playList.length;
|
||||
playMusic();
|
||||
break;
|
||||
case PlayMode.random:
|
||||
// 记录当前播放的索引
|
||||
_playHistory.add(musicModel.value.videoId!);
|
||||
_getRandomNumber();
|
||||
playMusic();
|
||||
break;
|
||||
case PlayMode.singleCycle:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 在列表范围内生成一个不包括当前索引的随机数
|
||||
void _getRandomNumber() {
|
||||
_currentIndex = NumUtil.getRandomNumberExcludingCurrent(0, playList.length, _currentIndex);
|
||||
}
|
||||
|
||||
/// 切换播放模式
|
||||
void switchPlayMode() {
|
||||
if (_playModeIndex == PlayMode.values.length - 1) {
|
||||
_playModeIndex = 0;
|
||||
} else {
|
||||
_playModeIndex++;
|
||||
}
|
||||
playMode.value = PlayMode.values[_playModeIndex];
|
||||
MusicBox().putPlayMode(playMode.value);
|
||||
}
|
||||
|
||||
/// 重置播放状态
|
||||
void _resetPlaybackStatus() {
|
||||
_player.stop();
|
||||
_player.seek(Duration.zero);
|
||||
totalDuration.value = Duration.zero;
|
||||
positionDuration.value = Duration.zero;
|
||||
bufferedDuration.value = Duration.zero;
|
||||
musicModel.update((model) => model = MusicModel());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/data/sideb/api/music_api.dart';
|
||||
import 'package:tone_snap/data/sideb/enum/browse_type.dart';
|
||||
import 'package:tone_snap/data/sideb/models/browse_model.dart';
|
||||
import 'package:tone_snap/data/sideb/models/home_model.dart';
|
||||
import 'package:tone_snap/components/view_state_widget.dart';
|
||||
import 'package:tone_snap/data/api/music_api.dart';
|
||||
import 'package:tone_snap/data/enum/browse_type.dart';
|
||||
import 'package:tone_snap/data/models/browse_model.dart';
|
||||
import 'package:tone_snap/data/models/home_model.dart';
|
||||
import 'package:tone_snap/routes/app_routes.dart';
|
||||
import 'package:tone_snap/utils/obj_util.dart';
|
||||
|
||||
class HomeController extends GetxController {
|
||||
var data = <HomeModel>[].obs;
|
||||
var viewState = ViewState.loading.obs;
|
||||
var homeList = <HomeModel>[].obs;
|
||||
String? visitorData;
|
||||
|
||||
@override
|
||||
@ -22,7 +24,7 @@ class HomeController extends GetxController {
|
||||
'prettyPrint': false,
|
||||
'browseId': 'FEmusic_home'
|
||||
};
|
||||
BrowseModel? browseModel = await MusicApi.browse(queryParameters: queryParameters);
|
||||
BrowseModel? browseModel = await MusicApi.browse<BrowseModel>(queryParameters: queryParameters, formJson: BrowseModel.fromMap);
|
||||
if (browseModel != null) {
|
||||
_extractAssemblyData(browseModel);
|
||||
|
||||
@ -50,7 +52,7 @@ class HomeController extends GetxController {
|
||||
'itct': clickTrackingParams,
|
||||
'prettyPrint': false
|
||||
};
|
||||
BrowseModel? browseModel = await MusicApi.browse(queryParameters: queryParameters, visitorData: visitorData);
|
||||
BrowseModel? browseModel = await MusicApi.browse<BrowseModel>(visitorData: visitorData, queryParameters: queryParameters, formJson: BrowseModel.fromMap);
|
||||
if (browseModel != null) {
|
||||
_extractAssemblyData(browseModel);
|
||||
|
||||
@ -109,6 +111,7 @@ class HomeController extends GetxController {
|
||||
} else if (runs[0].navigationEndpoint?.browseEndpoint != null) {
|
||||
model.browseType = runs[0].navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType;
|
||||
content.browseId = runs[0].navigationEndpoint?.browseEndpoint?.browseId;
|
||||
content.params = runs[0].navigationEndpoint?.browseEndpoint?.params;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -138,6 +141,7 @@ class HomeController extends GetxController {
|
||||
if (e2.musicTwoRowItemRenderer?.navigationEndpoint != null) {
|
||||
model.browseType = e2.musicTwoRowItemRenderer?.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType;
|
||||
content.browseId = e2.musicTwoRowItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId;
|
||||
content.params = e2.musicTwoRowItemRenderer?.navigationEndpoint?.browseEndpoint?.params;
|
||||
}
|
||||
}
|
||||
if (thumbnails != null && thumbnails.isNotEmpty) {
|
||||
@ -148,14 +152,20 @@ class HomeController extends GetxController {
|
||||
}
|
||||
// 根据类型判断是否添加
|
||||
if (BrowseTypeExtension.isThereAny(model.browseType)) {
|
||||
data.add(model);
|
||||
homeList.add(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
viewState.value = homeList.isNotEmpty ? ViewState.normal : ViewState.empty;
|
||||
}
|
||||
|
||||
/// 去播放音乐
|
||||
void goPlayMusic(Content content) {
|
||||
Get.toNamed(AppRoutes.playMusic, arguments: {'playlistId': content.playlistId, 'videoId': content.videoId});
|
||||
/// 打开播放页面
|
||||
void openPlayPage(Content content) {
|
||||
Get.toNamed(AppRoutes.playPage, arguments: {'playlistId': content.playlistId, 'videoId': content.videoId});
|
||||
}
|
||||
|
||||
/// 打开歌单/专辑页面
|
||||
void openSongSheet(Content content) {
|
||||
Get.toNamed(AppRoutes.album, arguments: {'browseId': content.browseId});
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/base_scrollbar.dart';
|
||||
import 'package:tone_snap/data/sideb/enum/browse_type.dart';
|
||||
import 'package:tone_snap/components/view_state_widget.dart';
|
||||
import 'package:tone_snap/data/enum/browse_type.dart';
|
||||
import 'package:tone_snap/data/models/home_model.dart';
|
||||
import 'package:tone_snap/modules/sideb/widgets/album_item.dart';
|
||||
import 'package:tone_snap/modules/sideb/widgets/atv_item.dart';
|
||||
import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart';
|
||||
import 'package:tone_snap/data/sideb/models/home_model.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/modules/sideb/widgets/omv_item.dart';
|
||||
import 'package:tone_snap/utils/obj_util.dart';
|
||||
@ -21,13 +22,13 @@ class HomeView extends GetView<HomeController> {
|
||||
Get.find<HomeController>();
|
||||
return Column(
|
||||
children: [
|
||||
_buildTitleWidget(),
|
||||
_buildTitle(),
|
||||
_buildListView(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitleWidget() {
|
||||
Widget _buildTitle() {
|
||||
return MusicAppbar(
|
||||
title: 'Musicoo',
|
||||
showBackWidget: false,
|
||||
@ -42,24 +43,74 @@ class HomeView extends GetView<HomeController> {
|
||||
|
||||
Widget _buildListView(BuildContext context) {
|
||||
return Expanded(
|
||||
child: BaseScrollbar(
|
||||
child: Obx(() {
|
||||
return ListView.separated(
|
||||
itemCount: controller.data.length,
|
||||
padding: EdgeInsets.fromLTRB(16.w, 0.h, 16.w, 16.h),
|
||||
itemBuilder: (context, index) {
|
||||
return _buildColumnItem(controller.data[index]);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return SizedBox(height: 16.h);
|
||||
},
|
||||
);
|
||||
}),
|
||||
child: Obx(() {
|
||||
return ViewStateWidget(
|
||||
viewState: controller.viewState.value,
|
||||
child: BaseScrollbar(
|
||||
child: ListView.separated(
|
||||
itemCount: controller.homeList.length,
|
||||
padding: EdgeInsets.fromLTRB(16.w, 0.h, 16.w, 16.h),
|
||||
itemBuilder: (context, index) {
|
||||
return _buildItem(controller.homeList[index]);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return SizedBox(height: 16.h);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(HomeModel model) {
|
||||
if (!BrowseTypeExtension.isThereAny(model.browseType)) {
|
||||
return Container();
|
||||
}
|
||||
double? itemHeight;
|
||||
if (model.browseType == BrowseType.musicVideoTypeAtv.name) {
|
||||
itemHeight = 174.h;
|
||||
} else if (model.browseType == BrowseType.musicPageTypeAlbum.name ||
|
||||
model.browseType == BrowseType.musicPageTypePlaylist.name) {
|
||||
itemHeight = 195.h;
|
||||
} else if (model.browseType == BrowseType.musicVideoTypeOmv.name) {
|
||||
itemHeight = 277.h;
|
||||
}
|
||||
return SizedBox(
|
||||
height: itemHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
_labelWidget(ObjUtil.getStr(model.headerTitle)),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: model.contents != null ? model.contents!.length : 0,
|
||||
itemBuilder: (context, index) {
|
||||
final content = model.contents![index];
|
||||
if (model.browseType == BrowseType.musicVideoTypeAtv.name) {
|
||||
return AtvItem(content: content,
|
||||
onTap: () => controller.openPlayPage(content));
|
||||
} else
|
||||
if (model.browseType == BrowseType.musicPageTypeAlbum.name ||
|
||||
model.browseType == BrowseType.musicPageTypePlaylist.name) {
|
||||
return AlbumItem(content: content,
|
||||
onTap: () => controller.openSongSheet(content));
|
||||
} else
|
||||
if (model.browseType == BrowseType.musicVideoTypeOmv.name) {
|
||||
return OmvItem(content: content);
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return SizedBox(width: 7.w);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 模版标签
|
||||
Widget _labelWidget(String label) {
|
||||
return InkWell(
|
||||
onTap: () {},
|
||||
@ -89,47 +140,4 @@ class HomeView extends GetView<HomeController> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 垂直列表Item
|
||||
Widget _buildColumnItem(HomeModel model) {
|
||||
if (!BrowseTypeExtension.isThereAny(model.browseType)) {
|
||||
return Container();
|
||||
}
|
||||
double? itemHeight;
|
||||
if (model.browseType == BrowseType.musicVideoTypeAtv.name) {
|
||||
itemHeight = 174.h;
|
||||
} else if (model.browseType == BrowseType.musicPageTypeAlbum.name || model.browseType == BrowseType.musicPageTypePlaylist.name) {
|
||||
itemHeight = 195.h;
|
||||
} else if (model.browseType == BrowseType.musicVideoTypeOmv.name) {
|
||||
itemHeight = 277.h;
|
||||
}
|
||||
return SizedBox(
|
||||
height: itemHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
_labelWidget(ObjUtil.getStr(model.headerTitle)),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: model.contents != null ? model.contents!.length : 0,
|
||||
itemBuilder: (context, index) {
|
||||
final content = model.contents![index];
|
||||
if (model.browseType == BrowseType.musicVideoTypeAtv.name) {
|
||||
return AtvItem(content: content, onTap: () => controller.goPlayMusic(content));
|
||||
} else if (model.browseType == BrowseType.musicPageTypeAlbum.name || model.browseType == BrowseType.musicPageTypePlaylist.name) {
|
||||
return AlbumItem(content: content);
|
||||
} else if (model.browseType == BrowseType.musicVideoTypeOmv.name) {
|
||||
return OmvItem(content: content);
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return SizedBox(width: 7.w);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/modules/sideb/home/home_view.dart';
|
||||
import 'package:tone_snap/modules/sideb/me/me_view.dart';
|
||||
import 'package:tone_snap/modules/sideb/personal_music_library/personal_music_library_view.dart';
|
||||
import 'package:tone_snap/modules/sideb/search_music/search_music_view.dart';
|
||||
|
||||
class InitialController extends GetxController {
|
||||
@ -11,7 +11,7 @@ class InitialController extends GetxController {
|
||||
final pages = [
|
||||
PageItem([Assets.sideBBnb1Unselected, Assets.sideBBnb1Selected], const HomeView()),
|
||||
PageItem([Assets.sideBBnb2Unselected, Assets.sideBBnb2Selected], const SearchMusicView()),
|
||||
PageItem([Assets.sideBBnb3Unselected, Assets.sideBBnb3Selected], const MeView()),
|
||||
PageItem([Assets.sideBBnb3Unselected, Assets.sideBBnb3Selected], const PersonalMusicLibraryView()),
|
||||
];
|
||||
var currentIndex = 0.obs;
|
||||
|
||||
|
||||
@ -15,28 +15,35 @@ class InitialView extends StatelessWidget {
|
||||
double bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
return Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
Assets.sideBHomeBg,
|
||||
width: 1.sw,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
_buildPageBg(),
|
||||
Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: PageView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: controller.pageController,
|
||||
children: controller.pages.map((e) => KeepAliveWrapper(child: e.widget)).toList(),
|
||||
),
|
||||
body: _buildBody(),
|
||||
bottomNavigationBar: _buildBottomAppBar(bottomPadding),
|
||||
),
|
||||
// _buildPlayerBar(bottomPadding),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPageBg() {
|
||||
return Image.asset(
|
||||
Assets.sideBHomeBg,
|
||||
width: 1.sw,
|
||||
fit: BoxFit.fitWidth,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return PageView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: controller.pageController,
|
||||
children: controller.pages.map((e) => KeepAliveWrapper(child: e.widget)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomAppBar(double bottomPadding) {
|
||||
return Container(
|
||||
height: 72.h + bottomPadding,
|
||||
height: kBottomNavigationBarHeight + bottomPadding,
|
||||
padding: EdgeInsets.only(bottom: bottomPadding),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(topLeft: Radius.circular(24.r), topRight: Radius.circular(24.r)),
|
||||
@ -79,28 +86,4 @@ class InitialView extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlayerBar(double bottomPadding) {
|
||||
return Positioned(
|
||||
bottom: 65.5.h + bottomPadding,
|
||||
left: 16.w,
|
||||
right: 16.w,
|
||||
child: Container(
|
||||
width: 1.sw,
|
||||
height: 75.5.h,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF80F988),
|
||||
borderRadius: BorderRadius.circular(36).r,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x40040604),
|
||||
offset: Offset(0, 4),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
10
lib/modules/sideb/love_songs/love_songs_binding.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'love_songs_controller.dart';
|
||||
|
||||
class LoveSongsBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => LoveSongsController());
|
||||
}
|
||||
}
|
||||
13
lib/modules/sideb/love_songs/love_songs_controller.dart
Normal file
@ -0,0 +1,13 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/data/models/music_model.dart';
|
||||
import 'package:tone_snap/data/storage/love_songs_box.dart';
|
||||
|
||||
class LoveSongsController extends GetxController {
|
||||
var loveList = <MusicModel>[];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loveList = LoveSongsBox().getList();
|
||||
}
|
||||
}
|
||||
107
lib/modules/sideb/love_songs/love_songs_view.dart
Normal file
@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/base_scrollbar.dart';
|
||||
import 'package:tone_snap/components/network_image_widget.dart';
|
||||
import 'package:tone_snap/data/models/music_model.dart';
|
||||
import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart';
|
||||
|
||||
import 'love_songs_controller.dart';
|
||||
|
||||
class LoveSongsView extends StatelessWidget {
|
||||
LoveSongsView({super.key});
|
||||
|
||||
final controller = Get.find<LoveSongsController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const MusicAppbar(
|
||||
title: 'Love Songs',
|
||||
),
|
||||
_buildTotal(),
|
||||
_buildListView(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTotal() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 34.h, bottom: 10.h, left: 18.w, right: 18.w),
|
||||
child: Text(
|
||||
'${controller.loveList.length} Songs',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListView() {
|
||||
return Expanded(
|
||||
child: BaseScrollbar(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.loveList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildItem(controller.loveList[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(MusicModel model) {
|
||||
return InkWell(
|
||||
onTap: (){},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 18.w),
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImageWidget(
|
||||
url: '',
|
||||
width: 60.w,
|
||||
height: 60.w,
|
||||
radius: 8.r,
|
||||
),
|
||||
SizedBox(width: 14.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'There For You',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
'234 songs',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF999999),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'me_controller.dart';
|
||||
|
||||
class MeBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => MeController());
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MeController extends GetxController {
|
||||
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'me_controller.dart';
|
||||
|
||||
class MeView extends GetView<MeController> {
|
||||
const MeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.find<MeController>();
|
||||
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'personal_music_library_controller.dart';
|
||||
|
||||
class PersonalMusicLibraryBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => PersonalMusicLibraryController());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/modules/sideb/collect_playlists/collect_playlists_view.dart';
|
||||
import 'package:tone_snap/modules/sideb/playlists/playlists_view.dart';
|
||||
import 'package:tone_snap/routes/app_routes.dart';
|
||||
|
||||
class PersonalMusicLibraryController extends GetxController with GetSingleTickerProviderStateMixin {
|
||||
late TabController tabController;
|
||||
final labels = ['Playlists', 'Collect Playlists'];
|
||||
final pages = [const PlaylistsView(), const CollectPlaylistsView()];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
tabController = TabController(length: labels.length, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
tabController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void onTapModule(int index) {
|
||||
if (index == 0) {
|
||||
Get.toNamed(AppRoutes.loveSongs);
|
||||
}
|
||||
if (index == 1) {
|
||||
Get.toNamed(AppRoutes.loveSongs);
|
||||
}
|
||||
if (index == 2) {
|
||||
Get.toNamed(AppRoutes.loveSongs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/keep_alive_wrapper.dart';
|
||||
import 'package:tone_snap/components/my_custom_indicator.dart';
|
||||
import 'package:tone_snap/components/network_image_widget.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/res/themes/app_colors.dart';
|
||||
|
||||
import 'personal_music_library_controller.dart';
|
||||
|
||||
class PersonalMusicLibraryView extends GetView<PersonalMusicLibraryController> {
|
||||
const PersonalMusicLibraryView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.find<PersonalMusicLibraryController>();
|
||||
return Stack(
|
||||
children: [
|
||||
_buildPageBg(),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTitle(),
|
||||
_buildModule(),
|
||||
_buildTabBar(),
|
||||
_buildTabBarView(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPageBg() {
|
||||
return Image.asset(
|
||||
Assets.sideBPersonalMusicLibraryBg,
|
||||
width: 1.sw,
|
||||
fit: BoxFit.fitWidth,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitle() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 16.w),
|
||||
child: Text(
|
||||
'Library',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28.sp,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModule() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 28.h, horizontal: 16.w),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildModuleItem('', Assets.sideBLoveSolid, 'Songs 122', () => controller.onTapModule(0)),
|
||||
_buildModuleItem('', Assets.sideBArtists, 'Artists 122', () => controller.onTapModule(1)),
|
||||
_buildModuleItem('', Assets.sideBOfflineDownload, 'Offline 122', () => controller.onTapModule(2)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModuleItem(String url, String labelImg, String label, Function() onTap) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 109.w,
|
||||
height: 109.w,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF242529),
|
||||
borderRadius: BorderRadius.circular(8).r,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: NetworkImageWidget(
|
||||
url: url,
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
placeholder: Assets.sideBPlaceholderLibrary,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 8.h,
|
||||
left: 8.w,
|
||||
right: 8.w,
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
labelImg,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
height: 32.h,
|
||||
margin: const EdgeInsets.symmetric(vertical: 12).h,
|
||||
child: TabBar(
|
||||
controller: controller.tabController,
|
||||
tabAlignment: TabAlignment.center,
|
||||
dividerHeight: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4).w,
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 12).w,
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
color: const Color(0xFF999999),
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
indicatorPadding: EdgeInsets.zero,
|
||||
indicator: MyCustomIndicator(
|
||||
indWidth: 16.w,
|
||||
indHeight: 2.h,
|
||||
color: seedColor,
|
||||
radius: 1.r,
|
||||
),
|
||||
tabs: controller.labels.map((e) => Tab(text: e)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBarView() {
|
||||
return Expanded(
|
||||
child: TabBarView(
|
||||
controller: controller.tabController,
|
||||
children: controller.pages.map((e) => KeepAliveWrapper(child: e)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'play_music_controller.dart';
|
||||
|
||||
class PlayMusicBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => PlayMusicController());
|
||||
}
|
||||
}
|
||||
@ -1,317 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/full_width_track_shape.dart';
|
||||
import 'package:tone_snap/components/my_marquee_text.dart';
|
||||
import 'package:tone_snap/components/network_image_widget.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart';
|
||||
import 'package:tone_snap/res/themes/app_colors.dart';
|
||||
import 'package:tone_snap/utils/obj_util.dart';
|
||||
|
||||
import 'play_music_controller.dart';
|
||||
|
||||
class PlayMusicView extends StatelessWidget {
|
||||
PlayMusicView({super.key});
|
||||
|
||||
final controller = Get.find<PlayMusicController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Obx(() {
|
||||
return Visibility(
|
||||
visible: ObjUtil.isNotEmptyStr(controller.thumbnail.value),
|
||||
child: NetworkImageWidget(
|
||||
url: controller.thumbnail.value,
|
||||
width: 1.sw,
|
||||
height: 1.sh,
|
||||
placeholder: Container(),
|
||||
errorWidget: Container(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
_buildPageBg(),
|
||||
Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20).w,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 12.h),
|
||||
_buildCover(),
|
||||
SizedBox(height: 36.h),
|
||||
_buildMusicName(),
|
||||
SizedBox(height: 24.h),
|
||||
_processBar(context),
|
||||
SizedBox(height: 100.h),
|
||||
_buildPlayController(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 页面占位背景
|
||||
Widget _buildPageBg() {
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 50.0, sigmaY: 50.0),
|
||||
child: Container(
|
||||
width: 1.sw,
|
||||
height: 1.sh,
|
||||
color: const Color(0x26000000),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 头部
|
||||
Widget _buildHeader() {
|
||||
return MusicAppbar(
|
||||
isDownBack: true,
|
||||
isBackBorder: true,
|
||||
actionOnTap: (){},
|
||||
titleWidget: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildMenuText('SONG', true),
|
||||
Container(
|
||||
width: 1.w,
|
||||
height: 14.h,
|
||||
color: const Color(0x73FFFFFF),
|
||||
),
|
||||
_buildMenuText('LYRICS', false),
|
||||
],
|
||||
),
|
||||
action: Image.asset(
|
||||
Assets.sideBCrossCircle,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 播放页/歌词页
|
||||
Widget _buildMenuText(String label, bool selected) {
|
||||
return InkWell(
|
||||
onTap: (){},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.5.w, vertical: 8.h),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Color(selected ? 0xD9FFFFFF : 0x73FFFFFF),
|
||||
fontSize: 15.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 封面
|
||||
Widget _buildCover() {
|
||||
return Obx(() {
|
||||
return NetworkImageWidget(
|
||||
url: controller.thumbnail.value,
|
||||
width: 1.sw,
|
||||
height: 330.h,
|
||||
radius: 16.r,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 歌名/作者信息、收藏、下载
|
||||
Widget _buildMusicName() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(() {
|
||||
return MyMarqueeText(
|
||||
text: ObjUtil.getStr(controller.title.value),
|
||||
textStyle: TextStyle(
|
||||
color: const Color(0xD9FFFFFF),
|
||||
fontSize: 22.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 6.h),
|
||||
Obx(() {
|
||||
return MyMarqueeText(
|
||||
text: ObjUtil.getStr(controller.subTitle.value),
|
||||
textStyle: TextStyle(
|
||||
color: const Color(0x99EEEEEE),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Image.asset(
|
||||
Assets.sideBLove,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
SizedBox(width: 20.w),
|
||||
Image.asset(
|
||||
Assets.sideBDownload,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 进度条
|
||||
Widget _processBar(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
activeTrackColor: sideBSeedColor,
|
||||
inactiveTrackColor: const Color(0x4DFFFFFF),
|
||||
secondaryActiveTrackColor: sideBSeedColor.withOpacity(0.3),
|
||||
trackHeight: 3.h,
|
||||
thumbColor: Colors.white,
|
||||
thumbShape: RoundSliderThumbShape(
|
||||
disabledThumbRadius: 7.w,
|
||||
enabledThumbRadius: 7.w,
|
||||
),
|
||||
trackShape: FullWidthTrackShape(),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 1.sw,
|
||||
height: 7.w,
|
||||
child: Obx(() {
|
||||
return Slider(
|
||||
value: controller.musicPlayerController.positionDuration.value.inSeconds.toDouble(),
|
||||
secondaryTrackValue: controller.musicPlayerController.bufferedDuration.value.inSeconds.toDouble(),
|
||||
min: 0,
|
||||
max: controller.musicPlayerController.totalDuration.value.inSeconds.toDouble(),
|
||||
onChanged: (value) => controller.musicPlayerController.seekToPosition(value),
|
||||
onChangeStart: (value) => controller.musicPlayerController.seekStartEnd(0),
|
||||
onChangeEnd: (value) => controller.musicPlayerController.seekStartEnd(1),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Obx(() {
|
||||
return Text(
|
||||
controller.musicPlayerController.getPositionDuration(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0xD9FFFFFF),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
Flexible(
|
||||
child: Obx(() {
|
||||
return Text(
|
||||
controller.musicPlayerController.getTotalDuration(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0x99FFFFFF),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 播放控制器
|
||||
Widget _buildPlayController() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: (){},
|
||||
child: Image.asset(
|
||||
Assets.sideBListLoop,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: controller.nextTrack,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 20).w,
|
||||
child: Image.asset(
|
||||
Assets.sideBPreviousSong,
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: controller.playPause,
|
||||
child: ClipOval(
|
||||
child: Container(
|
||||
width: 66.w,
|
||||
height: 66.w,
|
||||
color: Colors.white,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.none,
|
||||
child: Obx(() {
|
||||
return Image.asset(
|
||||
controller.musicPlayerController.isPlaying.value ? Assets.sideBPausePlay : Assets.sideBStartPlay,
|
||||
width: 26.w,
|
||||
height: 26.w,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20).w,
|
||||
child: GestureDetector(
|
||||
onTap: controller.nextTrack,
|
||||
child: Image.asset(
|
||||
Assets.sideBNextSong,
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: (){},
|
||||
child: Image.asset(
|
||||
Assets.sideBPlayList,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
9
lib/modules/sideb/play_page/play_page_binding.dart
Normal file
@ -0,0 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/modules/sideb/play_page/play_page_controller.dart';
|
||||
|
||||
class PlayPageBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => PlayPageController());
|
||||
}
|
||||
}
|
||||
@ -1,45 +1,52 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/base_easyloading.dart';
|
||||
import 'package:tone_snap/data/sideb/api/music_api.dart';
|
||||
import 'package:tone_snap/data/sideb/models/next_model.dart';
|
||||
import 'package:tone_snap/data/sideb/models/play_list_model.dart';
|
||||
import 'package:tone_snap/data/sideb/models/player_model.dart';
|
||||
import 'package:tone_snap/data/api/music_api.dart';
|
||||
import 'package:tone_snap/data/models/music_model.dart';
|
||||
import 'package:tone_snap/data/models/next_model.dart';
|
||||
import 'package:tone_snap/data/storage/love_songs_box.dart';
|
||||
import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart';
|
||||
import 'package:tone_snap/utils/obj_util.dart';
|
||||
|
||||
class PlayMusicController extends GetxController {
|
||||
class PlayPageController extends GetxController {
|
||||
var musicPlayerController = MusicPlayerController.to;
|
||||
|
||||
/// 是否从 MusicBar 打开
|
||||
bool isMusicBarOpen = false;
|
||||
|
||||
/// 是否添加到喜欢
|
||||
var isLove = false.obs;
|
||||
|
||||
/// 当前播放的歌曲信息
|
||||
late String videoId;
|
||||
late String playlistId;
|
||||
var thumbnail = Rx<String?>(null);
|
||||
var title = Rx<String?>(null);
|
||||
var subTitle = Rx<String?>(null);
|
||||
|
||||
/// 播放列表
|
||||
List<PlayListModel> playList = [];
|
||||
|
||||
/// 当前播放的歌曲下标
|
||||
int currentIndex = 0;
|
||||
String? playlistId;
|
||||
List<MusicModel>? playList;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
videoId = Get.arguments['videoId'] ?? '';
|
||||
playlistId = Get.arguments['playlistId'];
|
||||
videoId = Get.arguments['videoId'];
|
||||
playList = Get.arguments['playList'];
|
||||
isMusicBarOpen = Get.arguments['isMusicBarOpen'] ?? false;
|
||||
|
||||
isLove.value = LoveSongsBox().isLove(videoId);
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() async {
|
||||
super.onReady();
|
||||
_next();
|
||||
if (!isMusicBarOpen) {
|
||||
if (musicPlayerController.musicModel.value.videoId != videoId) {
|
||||
playList ??= await _next();
|
||||
musicPlayerController.setPlayList(videoId, playList!);
|
||||
musicPlayerController.playMusic();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> _next() async {
|
||||
BaseEasyLoading.loading();
|
||||
/// 获取播放列表
|
||||
Future<List<MusicModel>> _next() async {
|
||||
List<MusicModel> playList = [];
|
||||
NextModel? model = await MusicApi.next(playlistId: playlistId, videoId: videoId);
|
||||
BaseEasyLoading.dismiss();
|
||||
if (model != null) {
|
||||
var tabs = model.contents?.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer?.tabs;
|
||||
if (tabs != null && tabs.isNotEmpty) {
|
||||
@ -49,7 +56,7 @@ class PlayMusicController extends GetxController {
|
||||
var contents = o.tabRenderer?.content?.musicQueueRenderer?.content?.playlistPanelRenderer?.contents;
|
||||
if (contents != null && contents.isNotEmpty) {
|
||||
for (var j = 0; j < contents.length; ++j) {
|
||||
var playListModel = PlayListModel();
|
||||
var playListModel = MusicModel();
|
||||
var content = contents[j];
|
||||
// 封面
|
||||
var thumbnails = content.playlistPanelVideoRenderer?.thumbnail?.thumbnails;
|
||||
@ -81,56 +88,33 @@ class PlayMusicController extends GetxController {
|
||||
}
|
||||
}
|
||||
}
|
||||
_getCurrentMusicInfo();
|
||||
_player();
|
||||
}
|
||||
return playList;
|
||||
}
|
||||
|
||||
/// 加入/取消喜欢
|
||||
Future<void> addCancelLove() async {
|
||||
if (isLove.value) {
|
||||
await LoveSongsBox().delete(musicPlayerController.musicModel.value.videoId!);
|
||||
isLove.value = false;
|
||||
BaseEasyLoading.toast('Cancelled');
|
||||
} else {
|
||||
await LoveSongsBox().addData(musicPlayerController.musicModel.value.copyWith());
|
||||
isLove.value = true;
|
||||
BaseEasyLoading.toast('Collected');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前下标的歌曲信息
|
||||
void _getCurrentMusicInfo() {
|
||||
if (playList.isNotEmpty) {
|
||||
thumbnail.value = playList[currentIndex].thumbnail;
|
||||
title.value = playList[currentIndex].title;
|
||||
subTitle.value = playList[currentIndex].subTitle;
|
||||
/// 在播放列表中切换歌曲
|
||||
void switchMusic(int index, MusicModel model) {
|
||||
if (musicPlayerController.musicModel.value.videoId != model.videoId) {
|
||||
musicPlayerController.playMusicSpecifyIndex(index);
|
||||
}
|
||||
Get.back();
|
||||
}
|
||||
|
||||
Future<dynamic> _player() async {
|
||||
if (playList.isEmpty) return;
|
||||
BaseEasyLoading.loading();
|
||||
PlayerModel? model = await MusicApi.player(videoId: playList[currentIndex].videoId);
|
||||
BaseEasyLoading.dismiss();
|
||||
if (model != null && model.streamingData != null) {
|
||||
var formats = model.streamingData?.formats;
|
||||
if (formats != null && playList.isNotEmpty) {
|
||||
playList[currentIndex].url = formats[0].url ?? '';
|
||||
|
||||
// 开始播放
|
||||
musicPlayerController.playNewUrl(playList[currentIndex].url!, ObjUtil.getStr(playList[currentIndex].videoId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 继续/暂停
|
||||
void playPause() {
|
||||
musicPlayerController.playPause();
|
||||
}
|
||||
|
||||
/// 上一首
|
||||
Future<void> previousTrack() async {
|
||||
if (currentIndex > 0) {
|
||||
currentIndex--;
|
||||
}
|
||||
_getCurrentMusicInfo();
|
||||
_player();
|
||||
}
|
||||
|
||||
/// 下一首
|
||||
Future<void> nextTrack() async {
|
||||
if (currentIndex < playList.length - 1) {
|
||||
currentIndex++;
|
||||
}
|
||||
_getCurrentMusicInfo();
|
||||
_player();
|
||||
/// 删除播放列表中的歌曲
|
||||
void deleteMusic(int index) {
|
||||
musicPlayerController.playList.removeAt(index);
|
||||
}
|
||||
}
|
||||
535
lib/modules/sideb/play_page/play_page_view.dart
Normal file
@ -0,0 +1,535 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:tone_snap/components/base_scrollbar.dart';
|
||||
import 'package:tone_snap/components/full_width_track_shape.dart';
|
||||
import 'package:tone_snap/components/my_marquee_text.dart';
|
||||
import 'package:tone_snap/components/network_image_widget.dart';
|
||||
import 'package:tone_snap/data/enum/play_mode.dart';
|
||||
import 'package:tone_snap/data/models/music_model.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/modules/sideb/controllers/music_player_controller.dart';
|
||||
import 'package:tone_snap/modules/sideb/play_page/play_page_controller.dart';
|
||||
import 'package:tone_snap/modules/sideb/widgets/music_appbar.dart';
|
||||
import 'package:tone_snap/res/themes/app_colors.dart';
|
||||
import 'package:tone_snap/utils/date_util.dart';
|
||||
import 'package:tone_snap/utils/obj_util.dart';
|
||||
|
||||
class PlayPageView extends StatelessWidget {
|
||||
PlayPageView({super.key});
|
||||
|
||||
final controller = Get.find<PlayPageController>();
|
||||
final musicPlayerController = MusicPlayerController.to;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
_buildPageBgImg(),
|
||||
_buildPageBgColor(),
|
||||
Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20).w,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 12.h),
|
||||
_buildCover(),
|
||||
SizedBox(height: 36.h),
|
||||
_buildMusicName(),
|
||||
SizedBox(height: 24.h),
|
||||
_processBar(context),
|
||||
SizedBox(height: 24.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildPlayControlButtons(),
|
||||
SizedBox(height: 67.h),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 页面背景图
|
||||
Widget _buildPageBgImg() {
|
||||
return Obx(() {
|
||||
return Visibility(
|
||||
visible: ObjUtil.isNotEmptyStr(musicPlayerController.musicModel.value.thumbnail),
|
||||
child: NetworkImageWidget(
|
||||
url: musicPlayerController.musicModel.value.thumbnail,
|
||||
width: 1.sw,
|
||||
height: 1.sh,
|
||||
noPlaceholder: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 页面背景色
|
||||
Widget _buildPageBgColor() {
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 50.0, sigmaY: 50.0),
|
||||
child: Container(
|
||||
width: 1.sw,
|
||||
height: 1.sh,
|
||||
color: const Color(0x26000000),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 头部
|
||||
Widget _buildHeader() {
|
||||
return MusicAppbar(
|
||||
isDownBack: true,
|
||||
isBackBorder: true,
|
||||
actionOnTap: (){},
|
||||
titleWidget: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildMenuText('SONG', true),
|
||||
Container(
|
||||
width: 1.w,
|
||||
height: 14.h,
|
||||
color: const Color(0x73FFFFFF),
|
||||
),
|
||||
_buildMenuText('LYRICS', false),
|
||||
],
|
||||
),
|
||||
action: Image.asset(
|
||||
Assets.sideBCrossCircle,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 播放页/歌词页
|
||||
Widget _buildMenuText(String label, bool selected) {
|
||||
return InkWell(
|
||||
onTap: (){},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.5.w, vertical: 8.h),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Color(selected ? 0xD9FFFFFF : 0x73FFFFFF),
|
||||
fontSize: 15.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 封面
|
||||
Widget _buildCover() {
|
||||
return Obx(() {
|
||||
return NetworkImageWidget(
|
||||
url: musicPlayerController.musicModel.value.thumbnail,
|
||||
width: 1.sw,
|
||||
height: 330.h,
|
||||
radius: 16.r,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 歌名/作者信息、收藏、下载
|
||||
Widget _buildMusicName() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(() {
|
||||
return MyMarqueeText(
|
||||
text: ObjUtil.getStr(musicPlayerController.musicModel.value.title),
|
||||
textStyle: TextStyle(
|
||||
color: const Color(0xD9FFFFFF),
|
||||
fontSize: 22.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 6.h),
|
||||
Obx(() {
|
||||
return MyMarqueeText(
|
||||
text: ObjUtil.getStr(musicPlayerController.musicModel.value.subTitle),
|
||||
textStyle: TextStyle(
|
||||
color: const Color(0x99EEEEEE),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
ClipOval(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: controller.addCancelLove,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(6.w),
|
||||
child: Obx(() {
|
||||
return Image.asset(
|
||||
controller.isLove.value ? Assets.sideBLoveSolid : Assets.sideBLove,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
ClipOval(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: (){},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(6.w),
|
||||
child: Image.asset(
|
||||
Assets.sideBDownload,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 进度条
|
||||
Widget _processBar(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
activeTrackColor: seedColor,
|
||||
inactiveTrackColor: const Color(0x4DFFFFFF),
|
||||
secondaryActiveTrackColor: seedColor.withOpacity(0.3),
|
||||
trackHeight: 3.h,
|
||||
thumbColor: Colors.white,
|
||||
thumbShape: RoundSliderThumbShape(
|
||||
disabledThumbRadius: 7.w,
|
||||
enabledThumbRadius: 7.w,
|
||||
),
|
||||
trackShape: FullWidthTrackShape(),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 1.sw,
|
||||
height: 7.w,
|
||||
child: Obx(() {
|
||||
return Slider(
|
||||
value: musicPlayerController.positionDuration.value.inSeconds.toDouble(),
|
||||
secondaryTrackValue: musicPlayerController.bufferedDuration.value.inSeconds.toDouble(),
|
||||
min: 0,
|
||||
max: musicPlayerController.totalDuration.value.inSeconds.toDouble(),
|
||||
onChanged: (value) => musicPlayerController.seekToPosition(value),
|
||||
onChangeStart: (value) => musicPlayerController.seekStartEnd(0),
|
||||
onChangeEnd: (value) => musicPlayerController.seekStartEnd(1),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Obx(() {
|
||||
return Text(
|
||||
DateUtil.playDuration(musicPlayerController.positionDuration.value),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0xD9FFFFFF),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
Flexible(
|
||||
child: Obx(() {
|
||||
return Text(
|
||||
DateUtil.playDuration(musicPlayerController.totalDuration.value),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0x99FFFFFF),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 播放控制按钮
|
||||
Widget _buildPlayControlButtons() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10).w,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ClipOval(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: musicPlayerController.switchPlayMode,
|
||||
child: Obx(() {
|
||||
var playModeAssets = Assets.sideBListLoop;
|
||||
if (musicPlayerController.playMode.value == PlayMode.listLoop) {
|
||||
playModeAssets = Assets.sideBListLoop;
|
||||
} else if (musicPlayerController.playMode.value == PlayMode.random) {
|
||||
playModeAssets = Assets.sideBShufflePlayback;
|
||||
} else if (musicPlayerController.playMode.value == PlayMode.singleCycle) {
|
||||
playModeAssets = Assets.sideBSingleCycle;
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10).w,
|
||||
child: Image.asset(
|
||||
playModeAssets,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
ClipOval(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: musicPlayerController.previousTrack,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12).w,
|
||||
child: Obx(() {
|
||||
return Image.asset(
|
||||
Assets.sideBPreviousTrack,
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
color: musicPlayerController.playList.isNotEmpty ? Colors.white : const Color(0xFFa8a8a8),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ClipOval(
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
child: InkWell(
|
||||
onTap: musicPlayerController.playPause,
|
||||
child: SizedBox(
|
||||
width: 66.w,
|
||||
height: 66.w,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.none,
|
||||
child: Obx(() {
|
||||
return Visibility(
|
||||
visible: musicPlayerController.processingState.value == ProcessingState.ready
|
||||
|| musicPlayerController.processingState.value == ProcessingState.completed,
|
||||
replacement: Center(
|
||||
child: SizedBox(
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
child: const CircularProgressIndicator(
|
||||
color: seedColor,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Image.asset(
|
||||
musicPlayerController.isPlaying.value ? Assets.sideBPausePlay : Assets.sideBStartPlay,
|
||||
width: 26.w,
|
||||
height: 26.w,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ClipOval(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: musicPlayerController.nextTrack,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12).w,
|
||||
child: Obx(() {
|
||||
return Image.asset(
|
||||
Assets.sideBNextTrack,
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
color: musicPlayerController.playList.isNotEmpty ? Colors.white : const Color(0xFFa8a8a8),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ClipOval(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: _openPlayList,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10).w,
|
||||
child: Image.asset(
|
||||
Assets.sideBPlayList,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openPlayList() {
|
||||
Get.bottomSheet(
|
||||
isScrollControlled:true,
|
||||
Container(
|
||||
width: 1.sw,
|
||||
height: 0.6.sh,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A),
|
||||
borderRadius: BorderRadius.only(topLeft: Radius.circular(18.r), topRight: Radius.circular(18.r)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 30.h,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.none,
|
||||
child: Image.asset(
|
||||
Assets.sideBBottomSheetIndicator,
|
||||
width: 29.w,
|
||||
height: 4.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: BaseScrollbar(
|
||||
child: Obx(() {
|
||||
return ListView.builder(
|
||||
itemCount: musicPlayerController.playList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildPlayListItem(index, musicPlayerController.playList[index]);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlayListItem(int index, MusicModel model) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => controller.switchMusic(index, model),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8).h,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 18.w),
|
||||
NetworkImageWidget(
|
||||
url: model.thumbnail,
|
||||
width: 60.w,
|
||||
height: 60.w,
|
||||
radius: 10.r,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(() {
|
||||
return MyMarqueeText(
|
||||
enable: musicPlayerController.musicModel.value.videoId == model.videoId,
|
||||
text: ObjUtil.getStr(model.title),
|
||||
textStyle: TextStyle(
|
||||
color: musicPlayerController.musicModel.value.videoId == model.videoId
|
||||
? seedColor
|
||||
: const Color(0xD9FFFFFF),
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 8.h),
|
||||
Obx(() {
|
||||
return MyMarqueeText(
|
||||
enable: musicPlayerController.musicModel.value.videoId == model.videoId,
|
||||
text: ObjUtil.getStr(model.subTitle),
|
||||
textStyle: TextStyle(
|
||||
color: musicPlayerController.musicModel.value.videoId == model.videoId
|
||||
? seedColor
|
||||
: const Color(0x99FFFFFF),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
return Visibility(
|
||||
visible: musicPlayerController.musicModel.value.videoId != model.videoId,
|
||||
child: ClipOval(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => controller.deleteMusic(index),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10).w,
|
||||
child: Image.asset(
|
||||
Assets.sideBPlayListDelete,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
return SizedBox(width: musicPlayerController.musicModel.value.videoId != model.videoId ? 8.w : 16.w);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
10
lib/modules/sideb/playlists/playlists_binding.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'playlists_controller.dart';
|
||||
|
||||
class PlaylistsBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => PlaylistsController());
|
||||
}
|
||||
}
|
||||
12
lib/modules/sideb/playlists/playlists_controller.dart
Normal file
@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PlaylistsController extends GetxController {
|
||||
ScrollController scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
scrollController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
120
lib/modules/sideb/playlists/playlists_view.dart
Normal file
@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/components/base_scrollbar.dart';
|
||||
import 'package:tone_snap/components/network_image_widget.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
|
||||
import 'playlists_controller.dart';
|
||||
|
||||
class PlaylistsView extends GetView<PlaylistsController> {
|
||||
const PlaylistsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.find<PlaylistsController>();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildAddPlaylists(),
|
||||
_buildListView(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddPlaylists() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Playlists',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF999999),
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
ClipOval(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10).w,
|
||||
child: Image.asset(
|
||||
Assets.sideBPlaylistsAdd,
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListView() {
|
||||
return Expanded(
|
||||
child: BaseScrollbar(
|
||||
scrollController: controller.scrollController,
|
||||
child: ListView.builder(
|
||||
controller: controller.scrollController,
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildItem();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem() {
|
||||
return InkWell(
|
||||
onTap: (){},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 16.w),
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImageWidget(
|
||||
url: '',
|
||||
width: 60.w,
|
||||
height: 60.w,
|
||||
radius: 8.r,
|
||||
),
|
||||
SizedBox(width: 14.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'There For You',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
'234 songs',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF999999),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/modules/sideb/splash/splash_controller.dart';
|
||||
|
||||
class SplashBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => SplashController());
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/modules/sideb/splash/splash_controller.dart';
|
||||
import 'package:tone_snap/res/themes/app_colors.dart';
|
||||
|
||||
class SplashView extends StatelessWidget {
|
||||
SplashView({super.key});
|
||||
|
||||
final controller = Get.find<SplashController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
_buildImageBg(),
|
||||
_buildProgress(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageBg() {
|
||||
return Image.asset(
|
||||
Assets.sideBLaunchImage,
|
||||
width: 1.sw,
|
||||
height: 1.sh,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgress() {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
margin: const EdgeInsets.only(bottom: 60).h,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 0.5.sw,
|
||||
child: Obx(() {
|
||||
return LinearProgressIndicator(
|
||||
value: controller.processValue.value,
|
||||
backgroundColor: Colors.white,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(sideBSeedColor),
|
||||
borderRadius: BorderRadius.circular(8).r,
|
||||
);
|
||||
}),
|
||||
),
|
||||
SizedBox(height: 14.h),
|
||||
Text(
|
||||
'Resource Loading...',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -5,18 +5,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:tone_snap/components/network_image_widget.dart';
|
||||
import 'package:tone_snap/data/sideb/models/home_model.dart';
|
||||
import 'package:tone_snap/data/models/home_model.dart';
|
||||
import 'package:tone_snap/utils/obj_util.dart';
|
||||
|
||||
class AlbumItem extends StatelessWidget {
|
||||
const AlbumItem({super.key, required this.content});
|
||||
const AlbumItem({super.key, required this.content, required this.onTap});
|
||||
|
||||
final Content content;
|
||||
final Function() onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
width: 109.w,
|
||||
height: double.infinity,
|
||||
|
||||
@ -7,7 +7,7 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:tone_snap/components/network_image_widget.dart';
|
||||
import 'package:tone_snap/data/sideb/models/home_model.dart';
|
||||
import 'package:tone_snap/data/models/home_model.dart';
|
||||
import 'package:tone_snap/generated/assets.dart';
|
||||
import 'package:tone_snap/utils/obj_util.dart';
|
||||
|
||||
|
||||