diff --git a/lib/common/audio_player_controller.dart b/lib/common/audio_player_controller.dart new file mode 100644 index 0000000..0f6fad5 --- /dev/null +++ b/lib/common/audio_player_controller.dart @@ -0,0 +1,201 @@ +// audio_player_controller.dart + +import 'dart:async'; +import 'package:get/get.dart'; +import 'package:just_audio/just_audio.dart'; +import '../common_widget/Song_widegt.dart'; +import '../models/getMusicList_bean.dart'; +import '../common/download_manager.dart'; +import '../common_widget/app_data.dart'; +import '../api/api_music_list.dart'; + +class AudioPlayerController extends GetxController { + final audioPlayer = AudioPlayer(); + final downloadManager = Get.find(); + final appData = AppData(); + + // Observable values + final currentSongIndex = 0.obs; + final duration = Duration.zero.obs; + final position = Duration.zero.obs; + final isPlaying = false.obs; + final isLoading = false.obs; + final isRotating = false.obs; + final isDisposed = false.obs; + + // Current song info + final artistName = ''.obs; + final musicName = ''.obs; + final likesStatus = false.obs; + final collectionsStatus = false.obs; + + // Song lists + final songList = [].obs; + final ids = [].obs; + final songUrls = [].obs; + final artists = [].obs; + final musicNames = [].obs; + final likes = [].obs; + final collections = [].obs; + + StreamSubscription? _positionSubscription; + StreamSubscription? _durationSubscription; + StreamSubscription? _playerStateSubscription; + + void initWithSongs(List songs, int initialIndex) { + songList.value = songs; + currentSongIndex.value = initialIndex; + _initializeSongLists(); + _initializePlayer(); + } + + void _initializeSongLists() { + for (int i = 0; i < songList.length; i++) { + ids.add(songList[i].id); + songUrls.add(songList[i].musicurl ?? ''); + artists.add(songList[i].artist); + musicNames.add(songList[i].title); + likes.add(songList[i].likes ?? false); + collections.add(songList[i].collection ?? false); + } + _updateCurrentSongInfo(); + } + + void _initializePlayer() { + // Position updates + _positionSubscription = audioPlayer.positionStream.listen((pos) { + position.value = pos; + }); + + // Duration updates + _durationSubscription = audioPlayer.durationStream.listen((dur) { + duration.value = dur ?? Duration.zero; + }); + + // Player state updates + _playerStateSubscription = audioPlayer.playerStateStream.listen((state) { + // isPlaying.value = state.playing; + if (state.processingState == ProcessingState.completed) { + playNext(); + } + }); + + // Initial load + _loadAndPlayCurrentSong(); + } + + void _updateCurrentSongInfo() { + artistName.value = artists[currentSongIndex.value]; + musicName.value = musicNames[currentSongIndex.value]; + likesStatus.value = likes[currentSongIndex.value]; + collectionsStatus.value = collections[currentSongIndex.value]; + } + + Future toggleLike() async { + final currentIndex = currentSongIndex.value; + likesStatus.value = !likesStatus.value; + likes[currentIndex] = likesStatus.value; + } + + Future toggleCollection() async { + final currentIndex = currentSongIndex.value; + collectionsStatus.value = !collectionsStatus.value; + collections[currentIndex] = collectionsStatus.value; + } + + Future _loadAndPlayCurrentSong() async { + isLoading.value = true; + position.value = Duration.zero; + duration.value = Duration.zero; + _updateCurrentSongInfo(); + + await _checkAndUpdateSongStatus(currentSongIndex.value); + + try { + await audioPlayer.stop(); + + final localSong = downloadManager.getLocalSong(currentSongIndex.value); + final audioSource = localSong != null + ? AudioSource.file(localSong.musicurl!) + : AudioSource.uri(Uri.parse(songUrls[currentSongIndex.value])); + + await audioPlayer.setAudioSource(audioSource, preload: true); + duration.value = await audioPlayer.duration ?? Duration.zero; + await audioPlayer.play(); + } catch (e) { + print('Error loading audio source: $e'); + } finally { + isLoading.value = false; + } + } + + Future _checkAndUpdateSongStatus(int index) async { + if (songList[index].likes == null || songList[index].collection == null) { + try { + MusicListBean musicListBean = await GetMusic().getMusicById( + id: ids[index], + Authorization: appData.currentToken, + ); + + if (musicListBean.code == 200) { + likes[index] = musicListBean.likeOrNot!; + collections[index] = musicListBean.collectOrNot!; + + if (index == currentSongIndex.value) { + likesStatus.value = musicListBean.likeOrNot!; + collectionsStatus.value = musicListBean.collectOrNot!; + } + } + } catch (e) { + print('Error fetching song status: $e'); + } + } + } + + void playOrPause() async { + if (audioPlayer.playing) { + isPlaying.value = false; + await audioPlayer.pause(); + } else { + await audioPlayer.play(); + isPlaying.value = true; + } + } + + void playNext() { + if (currentSongIndex.value < songList.length - 1) { + currentSongIndex.value++; + } else { + currentSongIndex.value = 0; + } + _loadAndPlayCurrentSong(); + } + + void playPrevious() { + if (currentSongIndex.value > 0) { + currentSongIndex.value--; + } else { + currentSongIndex.value = songList.length - 1; + } + _loadAndPlayCurrentSong(); + } + + void seekTo(Duration position) async { + await audioPlayer.seek(position); + } + + void changeSong(int index) { + currentSongIndex.value = index; + _loadAndPlayCurrentSong(); + } + + @override + void onClose() { + isDisposed.value = true; + _positionSubscription?.cancel(); + _durationSubscription?.cancel(); + _playerStateSubscription?.cancel(); + audioPlayer.dispose(); + super.onClose(); + } +} \ No newline at end of file diff --git a/lib/common_widget/app_data.dart b/lib/common_widget/app_data.dart index cf17ae9..89e1ec4 100644 --- a/lib/common_widget/app_data.dart +++ b/lib/common_widget/app_data.dart @@ -7,5 +7,7 @@ class AppData extends GetxController{ String get currentToken => box.read('currentToken'); String get currentUsername => box.read('currentUsername') ?? '游客'; String get currentAvatar=> box.read('currentAvatar') ?? 'http://b.hiphotos.baidu.com/image/pic/item/e824b899a9014c08878b2c4c0e7b02087af4f4a3.jpg'; - + set currentToken(String token) => box.write('currentToken', token); + set currentUsername(String username) => box.write('currentUsername', username); + set currentAvatar(String avatar) => box.write('currentAvatar', avatar); } \ No newline at end of file diff --git a/lib/view/begin/begin_view.dart b/lib/view/begin/begin_view.dart index 84ea79b..783a635 100644 --- a/lib/view/begin/begin_view.dart +++ b/lib/view/begin/begin_view.dart @@ -17,139 +17,144 @@ class _BeginViewState extends State with TickerProviderStateMixin { @override void initState() { - tabController = TabController( - initialIndex: 0, - length: 2, - vsync: this - ); + tabController = TabController(initialIndex: 0, length: 2, vsync: this); + tabController.addListener(() { + if (!tabController.indexIsChanging) { + FocusScope.of(context).unfocus(); + } + }); super.initState(); } @override Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage("assets/img/app_bg.png"), - fit: BoxFit.cover, - ), - ), - child: Scaffold( - backgroundColor: Colors.transparent, - resizeToAvoidBottomInset: false, - body: ScrollConfiguration( - behavior: const ScrollBehavior().copyWith( - physics: const NeverScrollableScrollPhysics(), + return GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage("assets/img/app_bg.png"), + fit: BoxFit.cover, + ), ), - child: Column( - children: [ - // 顶部欢迎部分 - Padding( - padding: const EdgeInsets.only(top: 110, left: 40, right: 40), - child: Row( - children: [ - const Column( + child: Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + body: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith( + physics: const NeverScrollableScrollPhysics(), + ), + child: Column( + children: [ + // 顶部欢迎部分 + Padding( + padding: + const EdgeInsets.only(top: 110, left: 40, right: 40), + child: Row( children: [ - Text( - "你好吖喵星来客,", - style: TextStyle( - color: Colors.black, - fontSize: 20, - fontWeight: FontWeight.w500 - ), - ), - Row( + const Column( children: [ Text( - "欢迎来到", + "你好吖喵星来客,", style: TextStyle( color: Colors.black, fontSize: 20, - fontWeight: FontWeight.w500 - ), + fontWeight: FontWeight.w500), ), - Text( - "喵听", - style: TextStyle( - color: Colors.black, - fontSize: 32, - fontWeight: FontWeight.w800 - ), + Row( + children: [ + Text( + "欢迎来到", + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontWeight: FontWeight.w500), + ), + Text( + "喵听", + style: TextStyle( + color: Colors.black, + fontSize: 32, + fontWeight: FontWeight.w800), + ), + ], ), ], ), + const SizedBox(width: 25), + Image.asset("assets/img/app_logo.png", width: 80), ], ), - const SizedBox(width: 25), - Image.asset("assets/img/app_logo.png", width: 80), - ], - ), - ), + ), - const SizedBox(height: 20), // 添加一些间距 + const SizedBox(height: 20), // 添加一些间距 - // 剩余部分使用 Expanded - Expanded( - child: Column( - children: [ - // TabBar部分 - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: Material( - color: Colors.white, - elevation: 3, - borderRadius: BorderRadius.circular(10), - child: TabBar( - controller: tabController, - unselectedLabelColor: const Color(0xffCDCDCD), - labelColor: Colors.black, - indicatorSize: TabBarIndicatorSize.tab, - indicator: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: MColor.LGreen - ), - tabs: [ - Container( - padding: const EdgeInsets.all(8.0), - child: const Text( - '登录', - style: TextStyle( - fontSize: 20, + // 剩余部分使用 Expanded + Expanded( + child: Column( + children: [ + // TabBar部分 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), + child: Material( + color: Colors.white, + elevation: 3, + borderRadius: BorderRadius.circular(10), + child: TabBar( + controller: tabController, + unselectedLabelColor: const Color(0xffCDCDCD), + // onTap: (_) { + // FocusScope.of(context).unfocus(); + // }, + labelColor: Colors.black, + indicatorSize: TabBarIndicatorSize.tab, + indicator: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: MColor.LGreen), + tabs: [ + Container( + padding: const EdgeInsets.all(8.0), + child: const Text( + '登录', + style: TextStyle( + fontSize: 20, + ), + ), ), - ), - ), - Container( - padding: const EdgeInsets.all(8.0), - child: const Text( - '注册', - style: TextStyle( - fontSize: 20, + Container( + padding: const EdgeInsets.all(8.0), + child: const Text( + '注册', + style: TextStyle( + fontSize: 20, + ), + ), ), - ), + ], ), - ], + ), ), - ), - ), - // TabBarView部分 - Expanded( - child: TabBarView( - controller: tabController, - physics: const NeverScrollableScrollPhysics(), - children: const [ - LoginV(), - SignUpView(), - ], - ), - ) - ], - ), + // TabBarView部分 + Expanded( + child: TabBarView( + controller: tabController, + physics: const NeverScrollableScrollPhysics(), + children: const [ + LoginV(), + SignUpView(), + ], + ), + ) + ], + ), + ), + ], ), - ], + ), ), - ), - ), - ); + )); } -} \ No newline at end of file +} diff --git a/lib/view/begin/login_v.dart b/lib/view/begin/login_v.dart index 336cd47..738094a 100644 --- a/lib/view/begin/login_v.dart +++ b/lib/view/begin/login_v.dart @@ -27,6 +27,16 @@ class _LoginVState extends State { IconData iconPassword = CupertinoIcons.eye_fill; bool obscurePassword = true; + final _passwordFocusNode = FocusNode(); + final _nameFocusNode = FocusNode(); + + @override + void dispose() { + _passwordFocusNode.dispose(); + _nameFocusNode.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Form( @@ -39,6 +49,7 @@ class _LoginVState extends State { child: MyTextField( controller: nameController, hintText: '请输入账号', + focusNode: _nameFocusNode, obscureText: false, keyboardType: TextInputType.emailAddress, prefixIcon: Image.asset("assets/img/login_user.png"))), @@ -49,6 +60,7 @@ class _LoginVState extends State { child: MyTextField( controller: passwordController, hintText: '请输入密码', + focusNode: _passwordFocusNode, obscureText: obscurePassword, keyboardType: TextInputType.visiblePassword, prefixIcon: Image.asset("assets/img/login_lock.png"), @@ -78,11 +90,13 @@ class _LoginVState extends State { child: TextButton( onPressed: () async { try { + _nameFocusNode.unfocus(); + _passwordFocusNode.unfocus(); Get.dialog( Center( child: CircularProgressIndicator( - color: const Color(0xff429482), - backgroundColor: Colors.grey[200], + color: const Color(0xff429482), + backgroundColor: Colors.grey[200], ), ), barrierDismissible: false, // 防止用户点击背景关闭 @@ -99,11 +113,10 @@ class _LoginVState extends State { SnackBar( content: const Center( child: Text( - '登录成功!', + '登录成功', style: TextStyle( color: Colors.black, fontSize: 16.0, // 设置字体大小 - fontWeight: FontWeight.w500, // 可选:设置字体粗细 ), ), ), @@ -111,7 +124,8 @@ class _LoginVState extends State { behavior: SnackBarBehavior.floating, backgroundColor: Colors.white, elevation: 3, - width: 200, // 设置固定宽度 + width: 200, + // 设置固定宽度 shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), @@ -121,7 +135,6 @@ class _LoginVState extends State { .getInfo( Authorization: AppData().currentToken); } else { - Get.back(); throw Exception("账号或密码错误"); } } catch (error) { @@ -131,11 +144,10 @@ class _LoginVState extends State { SnackBar( content: Center( child: Text( - '${error.toString().replaceAll ('Exception: ', '')} !', - style: TextStyle( - color: Colors.red, + error.toString().replaceAll ('Exception: ', ''), + style: const TextStyle( + color: Colors.black, fontSize: 16.0, // 设置字体大小 - fontWeight: FontWeight.w500, // 可选:设置字体粗细 ), ), ), @@ -143,7 +155,11 @@ class _LoginVState extends State { behavior: SnackBarBehavior.floating, backgroundColor: Colors.white, elevation: 3, - width: 200, // 设置固定宽度 + margin: EdgeInsets.only( + bottom: 50, // 距离底部50像素 + right: (MediaQuery.of(context).size.width - 200) / 2, // 水平居中 + left: (MediaQuery.of(context).size.width - 200) / 2, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), diff --git a/lib/view/begin/setup_view.dart b/lib/view/begin/setup_view.dart index b9a4339..643b3a6 100644 --- a/lib/view/begin/setup_view.dart +++ b/lib/view/begin/setup_view.dart @@ -30,6 +30,12 @@ class _SignUpViewState extends State { bool obscurePassword = true; bool signUpRequired = false; + final _nameFocusNode = FocusNode(); + final _passwordFocusNode = FocusNode(); + final _emailFocusNode = FocusNode(); + final _confirmPSWFocusNode = FocusNode(); + final _confirmFocusNode = FocusNode(); + // 添加计时器相关变量 Timer? _timer; int _countDown = 60; @@ -75,6 +81,7 @@ class _SignUpViewState extends State { child: MyTextField( controller: nameController, hintText: '请输入用户名', + focusNode: _nameFocusNode, obscureText: false, keyboardType: TextInputType.emailAddress, prefixIcon: Image.asset("assets/img/login_user.png"), @@ -94,6 +101,7 @@ class _SignUpViewState extends State { controller: emailController, hintText: '请输入邮箱名', obscureText: false, + focusNode: _emailFocusNode, keyboardType: TextInputType.name, prefixIcon: Image.asset("assets/img/setup_email.png"), validator: (val) { @@ -110,6 +118,7 @@ class _SignUpViewState extends State { controller: passwordController, hintText: '请输入密码', obscureText: obscurePassword, + focusNode: _passwordFocusNode, keyboardType: TextInputType.visiblePassword, prefixIcon: Image.asset("assets/img/login_lock.png"), suffixIcon: IconButton( @@ -144,6 +153,7 @@ class _SignUpViewState extends State { controller: confirmPSWController, hintText: '请确认密码', obscureText: obscurePassword, + focusNode: _confirmPSWFocusNode, keyboardType: TextInputType.visiblePassword, prefixIcon: Image.asset("assets/img/login_lock.png"), suffixIcon: IconButton( @@ -184,6 +194,7 @@ class _SignUpViewState extends State { controller: confirmController, hintText: '请输入验证码', obscureText: false, + focusNode: _confirmFocusNode, keyboardType: TextInputType.name, prefixIcon: Image.asset("assets/img/setup_confirm.png"), validator: (val) { @@ -205,6 +216,11 @@ class _SignUpViewState extends State { ), onPressed: _canSendCode ? () async { + _nameFocusNode.unfocus(); + _emailFocusNode.unfocus(); + _passwordFocusNode.unfocus(); + _confirmPSWFocusNode.unfocus(); + _confirmFocusNode.unfocus(); UniversalBean bean = await SetupApiClient().verification( email: emailController.text, ); @@ -213,9 +229,9 @@ class _SignUpViewState extends State { startTimer(); // 发送成功后开始倒计时 ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Center( + content: const Center( child: Text( - '验证码已成功发送!', + '验证码发送成功', style: TextStyle(color: Colors.black), ), ), @@ -223,7 +239,11 @@ class _SignUpViewState extends State { behavior: SnackBarBehavior.floating, backgroundColor: Colors.white, elevation: 3, - width: 200, + margin: EdgeInsets.only( + bottom: 50, // 距离底部50像素 + right: (MediaQuery.of(context).size.width - 200) / 2, // 水平居中 + left: (MediaQuery.of(context).size.width - 200) / 2, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), @@ -232,13 +252,12 @@ class _SignUpViewState extends State { } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Center( + content: const Center( child: Text( - '邮箱为空或格式不正确!', + '请检查邮箱', style: TextStyle( color: Colors.black, fontSize: 16.0, // 设置字体大小 - fontWeight: FontWeight.w500, // 可选:设置字体粗细 ), ), ), @@ -246,7 +265,12 @@ class _SignUpViewState extends State { behavior: SnackBarBehavior.floating, backgroundColor: Colors.white, elevation: 3, - width: 220, + // width: 220, + margin: EdgeInsets.only( + bottom: 50, // 距离底部50像素 + right: (MediaQuery.of(context).size.width - 200) / 2, // 水平居中 + left: (MediaQuery.of(context).size.width - 200) / 2, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), @@ -271,6 +295,11 @@ class _SignUpViewState extends State { width: MediaQuery.of(context).size.width * 0.85, child: TextButton( onPressed: () async { + _nameFocusNode.unfocus(); + _emailFocusNode.unfocus(); + _passwordFocusNode.unfocus(); + _confirmPSWFocusNode.unfocus(); + _confirmFocusNode.unfocus(); if (_formKey.currentState?.validate() == false) { const Text( '', diff --git a/lib/view/main_tab_view/main_tab_view.dart b/lib/view/main_tab_view/main_tab_view.dart index fe75b10..d9b7fb8 100644 --- a/lib/view/main_tab_view/main_tab_view.dart +++ b/lib/view/main_tab_view/main_tab_view.dart @@ -5,6 +5,101 @@ import 'package:music_player_miao/view/user/user_view.dart'; import '../home_view.dart'; import '../release_view.dart'; +// 首先创建一个独立的迷你播放器组件 +class MiniPlayer extends StatelessWidget { + const MiniPlayer({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: 50, // 减小高度以适应底部 + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, -1), + ), + ], + ), + child: Row( + children: [ + // 歌曲封面 + Container( + width: 50, + height: 50, + color: Colors.grey[200], + child: Image.asset( + 'assets/img/artist_pic.png', + fit: BoxFit.cover, + ), + ), + // 歌曲信息 + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '背对背拥抱', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + '林俊杰', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + // 播放控制 + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.play_arrow), + onPressed: () {}, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + iconSize: 24, + ), + const SizedBox(width: 16), + IconButton( + icon: const Icon(Icons.playlist_play), + onPressed: () {}, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + iconSize: 24, + ), + const SizedBox(width: 8), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +} + class MainTabView extends StatefulWidget { const MainTabView({super.key}); @override @@ -38,63 +133,86 @@ class _MainTabViewState extends State with SingleTickerProviderStat return Scaffold( resizeToAvoidBottomInset: false, key: scaffoldKey, - body: TabBarView( - controller: controller, - children: const [ - HomeView(), - RankView(), - ReleaseView(), - UserView() - ], - ), - bottomNavigationBar: Container( - color: Colors.white, - child: TabBar( - controller: controller, - indicatorColor: Colors.transparent, - labelColor: Colors.black, - labelStyle: const TextStyle(fontSize: 12), // 减小文字大小 - unselectedLabelColor: const Color(0xffCDCDCD), - unselectedLabelStyle: const TextStyle(fontSize: 12), // 减小文字大小 - tabs: [ - Tab( - height: 60, // 明确指定高度 - icon: Image.asset( - selectTab == 0 ? "assets/img/home_tab.png" : "assets/img/home_tab_un.png", - width: 32, // 减小图标大小 - height: 32, - ), - text: "首页", - ), - Tab( - height: 60, - icon: Image.asset( - selectTab == 1 ? "assets/img/list_tab.png" : "assets/img/list_tab_un.png", - width: 32, - height: 32, - ), - text: "排行榜", - ), - Tab( - height: 60, - icon: Image.asset( - selectTab == 2 ? "assets/img/music_tab.png" : "assets/img/music_tab_un.png", - width: 32, - height: 32, + body: Stack( + children: [ + // TabBarView 占满整个屏幕 + Column( + children: [ + Expanded( + child: TabBarView( + controller: controller, + children: const [ + HomeView(), + RankView(), + ReleaseView(), + UserView() + ], + ), ), - text: "发布", + ], + ), + // 迷你播放器浮动在底部 + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const MiniPlayer(), + Container( + color: Colors.white, + child: TabBar( + controller: controller, + indicatorColor: Colors.transparent, + labelColor: Colors.black, + labelStyle: const TextStyle(fontSize: 12), + unselectedLabelColor: const Color(0xffCDCDCD), + unselectedLabelStyle: const TextStyle(fontSize: 12), + tabs: [ + Tab( + height: 60, + icon: Image.asset( + selectTab == 0 ? "assets/img/home_tab.png" : "assets/img/home_tab_un.png", + width: 32, + height: 32, + ), + text: "首页", + ), + Tab( + height: 60, + icon: Image.asset( + selectTab == 1 ? "assets/img/list_tab.png" : "assets/img/list_tab_un.png", + width: 32, + height: 32, + ), + text: "排行榜", + ), + Tab( + height: 60, + icon: Image.asset( + selectTab == 2 ? "assets/img/music_tab.png" : "assets/img/music_tab_un.png", + width: 32, + height: 32, + ), + text: "发布", + ), + Tab( + height: 60, + icon: Image.asset( + selectTab == 3 ? "assets/img/user_tab.png" : "assets/img/user_tab_un.png", + width: 32, + height: 32, + ), + text: "我的", + ), + ], + ), + ), + ], ), - Tab( - height: 60, - icon: Image.asset( - selectTab == 3 ? "assets/img/user_tab.png" : "assets/img/user_tab_un.png", - width: 32, - height: 32, - ), - text: "我的", - ), - ], - ), + ), + ], ), ); } diff --git a/lib/view/music_view_test.dart b/lib/view/music_view_test.dart new file mode 100644 index 0000000..1d62c97 --- /dev/null +++ b/lib/view/music_view_test.dart @@ -0,0 +1,507 @@ +// music_view_test.dart + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../common/audio_player_controller.dart'; +import '../common/download_manager.dart'; +import '../common_widget/Song_widegt.dart'; +import '../common_widget/app_data.dart'; +import 'comment_view.dart'; + +class MusicView extends StatefulWidget { + final List songList; + final int initialSongIndex; + final Function(int index, bool isCollected, bool isLiked)? + onSongStatusChanged; + + const MusicView({ + super.key, + required this.songList, + required this.initialSongIndex, + this.onSongStatusChanged, + }); + + @override + State createState() => _MusicViewState(); +} + +class _MusicViewState extends State + with SingleTickerProviderStateMixin { + // late AnimationController _rotationController; + final AudioPlayerController playerController = + Get.put(AudioPlayerController()); + final downloadManager = Get.find(); + final AppData appData = AppData(); + + @override + void initState() { + super.initState(); + playerController.initWithSongs(widget.songList, widget.initialSongIndex); + } + + @override + void dispose() { + super.dispose(); + } + + String formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, "0"); + String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); + String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); + return "$twoDigitMinutes:$twoDigitSeconds"; + } + + Widget _buildProgressSlider() { + return Obx(() { + final max = playerController.duration.value.inSeconds.toDouble() == 0 + ? 0.1 + : playerController.duration.value.inSeconds.toDouble(); + final current = + playerController.position.value.inSeconds.toDouble().clamp(0, max); + + return SliderTheme( + data: const SliderThemeData( + trackHeight: 3.0, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7.0), + overlayShape: RoundSliderOverlayShape(overlayRadius: 12.0), + ), + child: Slider( + min: 0, + max: max, + value: current.toDouble(), + onChanged: (value) { + playerController.seekTo(Duration(seconds: value.toInt())); + }, + activeColor: const Color(0xff429482), + inactiveColor: const Color(0xffE3F0ED), + ), + ); + }); + } + + Widget _buildPlayButton() { + return Obx(() { + return SizedBox( + width: 52, + height: 52, + child: Center( + child: playerController.isLoading.value + ? const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xff429482)), + strokeWidth: 3.0, + ) + : IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 52, + minHeight: 52, + maxWidth: 52, + maxHeight: 52, + ), + onPressed: playerController.playOrPause, + icon: !playerController.isPlaying.value + ? Image.asset( + "assets/img/music_play.png", + width: 52, + height: 52, + ) + : Image.asset( + "assets/img/music_pause.png", + width: 52, + height: 52, + ), + ), + ), + ); + }); + } + + // Widget _buildRotatingAlbumCover() { + // return Obx(() { + // final currentSong = + // widget.songList[playerController.currentSongIndex.value]; + // return RotationTransition( + // turns: _rotationController, + // child: Stack( + // alignment: Alignment.center, + // children: [ + // Positioned( + // child: ClipRRect( + // child: Image.network( + // currentSong.artistPic, + // width: 225, + // height: 225, + // fit: BoxFit.cover, + // ), + // ), + // ), + // ClipRRect( + // child: Image.asset( + // "assets/img/music_Ellipse.png", + // width: 350, + // height: 350, + // fit: BoxFit.cover, + // ), + // ), + // ], + // ), + // ); + // }); + // } + Widget _buildRotatingAlbumCover() { + return Obx(() { + final currentSong = + widget.songList[playerController.currentSongIndex.value]; + // 移除 RotationTransition,直接返回静态封面 + return Stack( + alignment: Alignment.center, + children: [ + Positioned( + child: ClipRRect( + child: Image.network( + currentSong.artistPic, + width: 225, + height: 225, + fit: BoxFit.cover, + ), + ), + ), + ClipRRect( + child: Image.asset( + "assets/img/music_Ellipse.png", + width: 350, + height: 350, + fit: BoxFit.cover, + ), + ), + ], + ); + }); + } + + void _showPlaylist() { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(30)), + ), + builder: (BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 15), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.45, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Center( + child: Text( + "播放列表", + style: TextStyle(fontSize: 20), + ), + ), + const SizedBox(height: 10), + Expanded( + child: Obx(() => ListView.builder( + itemCount: playerController.musicNames.length, + itemBuilder: (BuildContext context, int index) { + final isCurrentlyPlaying = + playerController.currentSongIndex.value == index; + return ListTile( + tileColor: isCurrentlyPlaying + ? const Color(0xffE3F0ED) + : null, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 20), + child: Text( + playerController.musicNames[index], + style: const TextStyle(fontSize: 18), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 20), + child: Image.asset( + "assets/img/songs_run.png", + width: 25, + ), + ), + ], + ), + onTap: () { + playerController.changeSong(index); + Navigator.pop(context); + }, + ); + }, + )), + ), + ], + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage("assets/img/app_bg.png"), + fit: BoxFit.cover, + ), + ), + child: Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 45, left: 10, right: 10), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + onPressed: () { + Get.back(); + }, + icon: Image.asset( + "assets/img/back.png", + width: 25, + height: 25, + fit: BoxFit.contain, + ), + ), + Row( + children: [ + IconButton( + onPressed: () {}, + icon: Image.asset( + "assets/img/music_add.png", + width: 30, + height: 30, + ), + ), + Obx(() => IconButton( + onPressed: downloadManager.isDownloading( + playerController.ids[playerController + .currentSongIndex.value]) || + downloadManager.isCompleted( + playerController.ids[playerController + .currentSongIndex.value]) + ? null + : () async { + await downloadManager.startDownload( + song: widget.songList[playerController + .currentSongIndex.value], + context: context, + ); + }, + icon: Obx(() { + if (downloadManager.isDownloading( + playerController.ids[playerController + .currentSongIndex.value])) { + return Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator( + value: downloadManager.getProgress( + playerController.ids[ + playerController + .currentSongIndex.value]), + backgroundColor: Colors.grey[200], + valueColor: + const AlwaysStoppedAnimation< + Color>(Color(0xff429482)), + strokeWidth: 3.0, + ), + ), + Text( + '${(downloadManager.getProgress(playerController.ids[playerController.currentSongIndex.value]) * 100).toInt()}', + style: const TextStyle(fontSize: 12), + ), + ], + ); + } + return Image.asset( + downloadManager.isCompleted( + playerController.ids[playerController + .currentSongIndex.value]) + ? "assets/img/music_download_completed.png" + : "assets/img/music_download.png", + width: 30, + height: 30, + ); + }), + )), + ], + ), + ], + ), + const SizedBox(height: 80), + Center(child: _buildRotatingAlbumCover()), + const SizedBox(height: 60), + Obx(() => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + playerController.musicName.value, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Text( + playerController.artistName.value, + style: const TextStyle(fontSize: 20), + ), + ], + ), + Row( + children: [ + IconButton( + onPressed: () async { + playerController.toggleLike(); + final currentIndex = + playerController.currentSongIndex.value; + widget.onSongStatusChanged?.call( + currentIndex, + playerController.collections[currentIndex], + playerController.likes[currentIndex], + ); + }, + icon: Image.asset( + playerController.likesStatus.value + ? "assets/img/music_good.png" + : "assets/img/music_good_un.png", + width: 29, + height: 29, + ), + ), + IconButton( + onPressed: () async { + playerController.toggleCollection(); + final currentIndex = + playerController.currentSongIndex.value; + widget.onSongStatusChanged?.call( + currentIndex, + playerController.collections[currentIndex], + playerController.likes[currentIndex], + ); + }, + icon: Image.asset( + playerController.collectionsStatus.value + ? "assets/img/music_star.png" + : "assets/img/music_star_un.png", + width: 29, + height: 29, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CommentView( + id: playerController.ids[playerController + .currentSongIndex.value], + song: playerController.musicName.value, + singer: playerController.artistName.value, + cover: widget + .songList[playerController + .currentSongIndex.value] + .artistPic, + ), + ), + ); + }, + icon: Image.asset( + "assets/img/music_commend_un.png", + width: 29, + height: 29, + ), + ), + ], + ), + ], + )), + const SizedBox(height: 80), + Obx(() => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + formatDuration(playerController.position.value), + style: const TextStyle(color: Colors.black), + ), + Expanded(child: _buildProgressSlider()), + Text( + formatDuration(playerController.duration.value), + style: const TextStyle(color: Colors.black), + ), + ], + )), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + onPressed: () {}, + icon: Image.asset( + "assets/img/music_random.png", + width: 35, + height: 35, + ), + ), + Row( + children: [ + IconButton( + onPressed: playerController.playPrevious, + icon: Image.asset( + "assets/img/music_back.png", + width: 42, + height: 42, + ), + ), + const SizedBox(width: 10), + _buildPlayButton(), + const SizedBox(width: 10), + IconButton( + onPressed: playerController.playNext, + icon: Image.asset( + "assets/img/music_next.png", + width: 42, + height: 42, + ), + ), + ], + ), + IconButton( + onPressed: _showPlaylist, + icon: Image.asset( + "assets/img/music_more.png", + width: 35, + height: 35, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/view/user/user_view.dart b/lib/view/user/user_view.dart index 511ea60..b7fad99 100644 --- a/lib/view/user/user_view.dart +++ b/lib/view/user/user_view.dart @@ -423,6 +423,9 @@ class _UserViewState extends State with AutomaticKeepAliveClientMixin UniversalBean bean = await LogoutApiClient().logout( Authorization: AppData().currentToken, ); + AppData().currentToken = ''; + AppData().currentUsername = ''; + AppData().currentAvatar = ''; }, icon: Image.asset("assets/img/user_out.png"), iconSize: 60,