diff --git a/assets/img/like.png b/assets/img/like.png new file mode 100644 index 0000000..ca505ab Binary files /dev/null and b/assets/img/like.png differ diff --git a/assets/img/unlike.png b/assets/img/unlike.png new file mode 100644 index 0000000..6d1fb29 Binary files /dev/null and b/assets/img/unlike.png differ diff --git a/lib/api/api_music_list.dart b/lib/api/api_music_list.dart index eb9123a..79e23bd 100644 --- a/lib/api/api_music_list.dart +++ b/lib/api/api_music_list.dart @@ -1,9 +1,12 @@ import 'package:dio/dio.dart'; import '../common_widget/Song_widegt.dart'; +import '../models/MusicsListBean.dart'; import '../models/getMusicList_bean.dart'; import '../models/getRank_bean.dart'; +const String _getMusicList = "http://8.210.250.29:10010/musics/three-random"; +const String _getMusic = 'http://8.210.250.29:10010/musics/'; const String _getMusic1 = 'http://8.210.250.29:10010/musics/1'; const String _getMusic2 = 'http://8.210.250.29:10010/musics/2'; const String _getMusic3 = 'http://8.210.250.29:10010/musics/3'; @@ -13,6 +16,34 @@ const String _getSongDetail = 'http://8.210.250.29:10010/musics'; class GetMusic { final Dio dio = Dio(); + Future getMusicList({required String Authorization}) async { + Response response = await dio.get( + _getMusicList, + data: { + 'Authorization': Authorization, + }, + options: Options(headers: { + 'Authorization': Authorization, + 'Content-Type': 'application/json;charset=UTF-8' + })); + return MusicsListBean.fromMap(response.data); + } + + Future getMusicById({required int id, required String Authorization}) async { + print(_getMusic + id.toString()); + Response response = await dio.get( + _getMusic + id.toString(), + data: { + 'Authorization': Authorization, + }, + options: Options(headers: { + 'Authorization': Authorization, + 'Content-Type': 'application/json;charset=UTF-8' + })); + print(response.data); + return MusicListBean.formMap(response.data); + } + Future getMusic1({required String Authorization}) async { Response response = await dio.get( _getMusic1, @@ -26,6 +57,7 @@ class GetMusic { print(response.data); return MusicListBean.formMap(response.data); } + Future getMusic2({required String Authorization}) async { Response response = await dio.get( _getMusic2, diff --git a/lib/api/api_music_return.dart b/lib/api/api_music_return.dart index 6612bb8..26cc290 100644 --- a/lib/api/api_music_return.dart +++ b/lib/api/api_music_return.dart @@ -42,7 +42,7 @@ class commentMusic { _postComment, data: { 'content': content, - 'MusicId': musicId, + 'musicId': musicId, 'Authorization':Authorization }, @@ -57,9 +57,9 @@ class commentMusic { class getCommentApi { final Dio dio = Dio(); Future getComment({ - required String musicId, - required String pageNo, - required String pageSize, + required int musicId, + required int pageNo, + required int pageSize, required String Authorization, }) async { Response response = await dio.get( 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/download_manager.dart b/lib/common/download_manager.dart index 1394b4c..79c85ff 100644 --- a/lib/common/download_manager.dart +++ b/lib/common/download_manager.dart @@ -121,7 +121,7 @@ class DownloadManager extends GetxController { bool removeSong(int id) { if (_downloads[id.toString()]?.isCompleted ?? false) { - File file = File.fromUri(Uri.parse(_downloads[id.toString()]!.song.musicurl)); + File file = File.fromUri(Uri.parse(_downloads[id.toString()]!.song.musicurl!)); file.deleteSync(); _downloads.remove(id.toString()); _saveDownloadsToPrefs(); @@ -156,7 +156,7 @@ class DownloadManager extends GetxController { try { final filePath = await downloadApi.downloadMusic( - musicUrl: song.musicurl, + musicUrl: song.musicurl!, name: fileName, context: context, onProgress: (progress) { @@ -169,7 +169,7 @@ class DownloadManager extends GetxController { downloadItem.isCompleted = true; downloadItem.isDownloading = false; downloadItem.progress = 1.0; - song.musicurl = _getLocalAudioPath(fileName, song.musicurl); + song.musicurl = _getLocalAudioPath(fileName, song.musicurl!); print(song.musicurl); } else { downloadItem.isDownloading = false; diff --git a/lib/common_widget/Song_widegt.dart b/lib/common_widget/Song_widegt.dart index 1b3b92e..2c130ef 100644 --- a/lib/common_widget/Song_widegt.dart +++ b/lib/common_widget/Song_widegt.dart @@ -3,10 +3,10 @@ class Song { String artistPic; String title; String artist; - String musicurl; + String? musicurl; int id; - bool likes; - bool collection; + bool? likes; + bool? collection; // 构造函数 Song({ 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/models/MusicsListBean.dart b/lib/models/MusicsListBean.dart new file mode 100644 index 0000000..d0253f6 --- /dev/null +++ b/lib/models/MusicsListBean.dart @@ -0,0 +1,35 @@ +class MusicsListBean { + int? code; + String? msg; + List? data; + + MusicsListBean.fromMap(Map map) { + code = map['code']; + msg = map['msg']; + if (map['data'] != null && map['data'] is List) { + data = (map['data'] as List).map((item) => MusicItem.fromMap(item)).toList(); + } + } +} + +class MusicItem { + int? id; + String? name; + String? coverPath; + String? musicPath; + String? singerName; + String? uploadUserName; + bool? likeOrNot; + bool? collectOrNot; + + MusicItem.fromMap(Map map) { + id = map['id']; + name = map['name']; + coverPath = map['coverPath']; + musicPath = map['musicPath']; + singerName = map['singerName']; + uploadUserName = map['uploadUserName']; + likeOrNot = map['likeOrNot']; + collectOrNot = map['collectOrNot']; + } +} \ No newline at end of file diff --git a/lib/models/getRank_bean.dart b/lib/models/getRank_bean.dart index 574d5d1..74dda9d 100644 --- a/lib/models/getRank_bean.dart +++ b/lib/models/getRank_bean.dart @@ -24,8 +24,6 @@ class DataBean { String? musicPath; String? name; - - DataBean._formMap(Map map) { id = map['id']; singerName = map['singerName']; 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 ef0c495..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,10 +90,13 @@ class _LoginVState extends State { child: TextButton( onPressed: () async { try { + _nameFocusNode.unfocus(); + _passwordFocusNode.unfocus(); Get.dialog( Center( child: CircularProgressIndicator( - backgroundColor: Colors.grey[200], + color: const Color(0xff429482), + backgroundColor: Colors.grey[200], ), ), barrierDismissible: false, // 防止用户点击背景关闭 @@ -98,11 +113,10 @@ class _LoginVState extends State { SnackBar( content: const Center( child: Text( - '登录成功!', + '登录成功', style: TextStyle( color: Colors.black, fontSize: 16.0, // 设置字体大小 - fontWeight: FontWeight.w500, // 可选:设置字体粗细 ), ), ), @@ -110,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), ), @@ -120,7 +135,6 @@ class _LoginVState extends State { .getInfo( Authorization: AppData().currentToken); } else { - Get.back(); throw Exception("账号或密码错误"); } } catch (error) { @@ -130,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, // 可选:设置字体粗细 ), ), ), @@ -142,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/commend_view.dart b/lib/view/commend_view.dart deleted file mode 100644 index 36ef0c5..0000000 --- a/lib/view/commend_view.dart +++ /dev/null @@ -1,329 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:music_player_miao/api/api_music_return.dart'; -import 'package:music_player_miao/common_widget/app_data.dart'; -import 'package:music_player_miao/models/getComment_bean.dart'; -import 'package:music_player_miao/models/universal_bean.dart'; -import 'package:music_player_miao/widget/text_field.dart'; - - -class CommentView extends StatefulWidget { - - - @override - _CommentViewState createState() => _CommentViewState(); - - CommentView({super.key, required this.initialSongIndex}); - - late final int initialSongIndex; -} - -class _CommentViewState extends State { - List comments = []; - - TextEditingController commentController = TextEditingController(); - FocusNode commentFocusNode = FocusNode(); - List commentTimes = []; - List commentHeader = []; - List commentName = []; - bool ascendingOrder = true; - int playlistCount = 0; - - @override - void initState() { - super.initState(); - _fetchSonglistData(); - } - - Future _fetchSonglistData() async { - try { - GetCommentBean bean1 = await getCommentApi().getComment( - musicId: '1', - pageNo: '0', - pageSize: '10', - Authorization: AppData().currentToken, - ); - - // 检查 rows 是否为空 - if (bean1.rows == null) { - print('No comments found'); - return; - } - - setState(() { - comments = bean1.rows!.map((rows) => rows.content ?? 'No content').toList(); - commentTimes = bean1.rows!.map((rows) => rows.time ?? 'Unknown time').toList(); - commentHeader = bean1.rows!.map((rows) => rows.avatar ?? 'Default avatar').toList(); - commentName = bean1.rows!.map((rows) => rows.username ?? 'Anonymous').toList(); - playlistCount = comments.length; - }); - } catch (error) { - print('Error fetching songlist data: $error'); - } - } - - - - - @override - Widget build(BuildContext context) { - return Scaffold( - // backgroundColor: Colors.transparent, - appBar: AppBar( - centerTitle: true, - backgroundColor: const Color(0xffF6FFD1), - title: const Text( - '评论(200)', - style: TextStyle( - color: Colors.black, - fontSize: 22 - ), - ), - elevation: 0, - leading: IconButton( - onPressed: () { - Get.back(); - }, - icon: Image.asset( - "assets/img/back.png", - width: 25, - height: 25, - fit: BoxFit.contain, - ), - ), - ), - body: Column( - children: [ - Container( - height: 80, - padding: const EdgeInsets.only(left: 20, right: 10), - decoration: BoxDecoration( - color: const Color(0xffF9F2AF), - borderRadius: BorderRadius.circular(20) - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Image.asset( - "assets/img/artist_pic.png", - width: 60, - height: 60, - fit: BoxFit.cover, - ), - ), - const SizedBox(width: 20,), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "背对背拥抱", - maxLines: 1, - style: TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w400), - ), - Text( - "林俊杰", - maxLines: 1, - style: TextStyle(color: Colors.black, fontSize: 14), - ) - ], - ), - ], - ), - IconButton( - onPressed: () {}, - icon: Image.asset( - "assets/img/music_pause.png", - width: 25, - height: 25, - fit: BoxFit.contain, - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "评论区", - style: TextStyle( - fontSize: 18 - ), - ), - Row( - children: [ - const Text( - "时间", - style: TextStyle( - fontSize: 16 - ), - ), - IconButton( - onPressed: () { - setState(() { - ascendingOrder = !ascendingOrder; - - // 使用时间排序索引列表 - List sortedIndexes = List.generate(comments.length, (i) => i); - sortedIndexes.sort((a, b) { - DateTime timeA = DateTime.parse(commentTimes[a]); - DateTime timeB = DateTime.parse(commentTimes[b]); - return ascendingOrder ? timeA.compareTo(timeB) : timeB.compareTo(timeA); - }); - - comments = [for (var i in sortedIndexes) comments[i]]; - commentTimes = [for (var i in sortedIndexes) commentTimes[i]]; - commentHeader = [for (var i in sortedIndexes) commentHeader[i]]; - commentName = [for (var i in sortedIndexes) commentName[i]]; - }); - }, - icon: Image.asset( - ascendingOrder - ? "assets/img/commend_up.png" - : "assets/img/commend_down.png", - fit: BoxFit.contain, - ), - ), - - ], - ) - ], - ), - ), - /// 显示评论的区域 - Expanded( - child: - ListView.builder( - itemCount: comments.length, - itemBuilder: (context, index) { - return ListTile( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - CircleAvatar( - backgroundImage: NetworkImage(commentHeader[index]) - ), - const SizedBox(width: 10,), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(commentName[index], - style: const TextStyle(fontSize: 18)), - const SizedBox(width: 8), - // Adjust the spacing between elements - Text(commentTimes[index], - style: const TextStyle(fontSize: 14),), - // Add the timestamp - ], - ), - ], - ), // Ad - Padding( - padding: const EdgeInsets.only( - left: 50, top: 10, bottom: 20), - child: Text( - comments[index], - style: const TextStyle( - fontSize: 18), // Customize the font size if needed - ), - ), - Container( - width: 560, - height: 2, - color: const Color(0xffE3F0ED), - ) - ], - ), - ); - }, - ), - ), - - ///输入框和提交按钮 - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: TextFieldColor( - controller: commentController, - hintText: '来发表你的评论吧!', - ) - ), - const SizedBox(width: 8.0), - ElevatedButton( - onPressed: () async { - submitComment(); - UniversalBean bean = await commentMusic().comment( - musicId: widget.initialSongIndex, - content: commentController.text, - Authorization: AppData().currentToken); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xff429482), - // Change Colors.blue to your desired background color - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 10), // Adjust the radius as needed - ), - minimumSize: const Size(30, 44), - ), - child: const Text( - '提交', - style: TextStyle( - fontSize: 16 - ), - ), - ), - ], - ), - ), - ], - ), - ); - } - - void submitComment() async { - String comment = commentController.text.trim(); - if (comment.isEmpty) { - print('Comment cannot be empty'); - Get.snackbar('错误', '评论不能为空'); - return; - } - - print('Submitting comment with content: $comment'); - - try { - UniversalBean bean = await commentMusic().comment( - musicId: widget.initialSongIndex, - content: comment, - Authorization: AppData().currentToken, - ); - - // 处理响应结果 - if (bean.code == 200) { - print('Comment submitted successfully'); - commentController.clear(); - _fetchSonglistData(); // 刷新评论列表 - } else { - print('Failed to submit comment: ${bean.msg}'); - Get.snackbar('错误', bean.msg ?? '评论提交失败'); - } - } catch (error) { - print('Error submitting comment: $error'); - } - } - -} \ No newline at end of file diff --git a/lib/view/comment_view.dart b/lib/view/comment_view.dart new file mode 100644 index 0000000..4ad4149 --- /dev/null +++ b/lib/view/comment_view.dart @@ -0,0 +1,436 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:music_player_miao/api/api_music_return.dart'; +import 'package:music_player_miao/common_widget/app_data.dart'; +import 'package:music_player_miao/models/getComment_bean.dart'; +import 'package:music_player_miao/models/universal_bean.dart'; +import 'package:music_player_miao/widget/text_field.dart'; + +class CommentView extends StatefulWidget { + final int id; + final String song; + final String singer; + final String cover; + + const CommentView({ + super.key, + required this.id, + required this.song, + required this.singer, + required this.cover, + }); + + @override + _CommentViewState createState() => _CommentViewState(); +} + +class _CommentViewState extends State { + List comments = []; + ScrollController _scrollController = ScrollController(); + TextEditingController commentController = TextEditingController(); + FocusNode commentFocusNode = FocusNode(); + List commentTimes = []; + List commentHeader = []; + List commentName = []; + bool ascendingOrder = true; + int _page = 1; + int _pageSize = 10; + int _total = 0; + bool _isLoading = false; + bool _hasMoreData = true; + bool _isSendingComment = false; + + String avatar = AppData().currentAvatar; + String username = AppData().currentUsername; + + bool _isInitialLoading = true; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_scrollListener); + _fetchCommentData(); + commentController.addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + _scrollController.dispose(); + commentController.dispose(); + super.dispose(); + } + + void _scrollListener() { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { + if (!_isLoading && _hasMoreData) { + _loadMoreData(); + } + } + } + + Future _loadMoreData() async { + if (_isLoading || !_hasMoreData) return; + + setState(() { + _isLoading = true; + }); + + _page++; + await _fetchCommentData(); + + setState(() { + _isLoading = false; + }); + } + + String formatDateTime(String dateTimeStr) { + try { + DateTime dateTime = DateTime.parse(dateTimeStr); + // 格式化时间,只保留年月日时分 + return "${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}"; + } catch (e) { + return dateTimeStr; // 如果解析失败,返回原始字符串 + } + } + + Future _fetchCommentData() async { + print('Fetching page $_page'); + try { + GetCommentBean bean1 = await getCommentApi().getComment( + musicId: widget.id, + pageNo: _page, + pageSize: _pageSize, + Authorization: AppData().currentToken, + ); + + if (bean1.rows == null || bean1.rows!.isEmpty) { + setState(() { + _hasMoreData = false; + _isInitialLoading = false; + }); + return; + } + + _total = bean1.total!; + + setState(() { + if (_page == 1) { + comments = + bean1.rows!.map((rows) => rows.content ?? 'No content').toList(); + commentTimes = bean1.rows! + .map((rows) => formatDateTime(rows.time ?? 'Unknown time')) + .toList(); + commentHeader = bean1.rows! + .map((rows) => rows.avatar ?? 'Default avatar') + .toList(); + commentName = + bean1.rows!.map((rows) => rows.username ?? 'Anonymous').toList(); + } else { + comments.addAll( + bean1.rows!.map((rows) => rows.content ?? 'No content').toList()); + commentTimes.addAll(bean1.rows! + .map((rows) => formatDateTime(rows.time ?? 'Unknown time')) + .toList()); + commentHeader.addAll(bean1.rows! + .map((rows) => rows.avatar ?? 'Default avatar') + .toList()); + commentName.addAll( + bean1.rows!.map((rows) => rows.username ?? 'Anonymous').toList()); + } + + _hasMoreData = comments.length < _total; + _isInitialLoading = false; + }); + } catch (error) { + print('Error fetching comment data: $error'); + setState(() { + _isLoading = false; + _isInitialLoading = false; + }); + } + } + + void _sortComments() { + setState(() { + comments = comments.reversed.toList(); + commentTimes = commentTimes.reversed.toList(); + commentHeader = commentHeader.reversed.toList(); + commentName = commentName.reversed.toList(); + }); + } + + // 新增:检查评论是否有效 + bool isCommentValid() { + String comment = commentController.text.trim(); + return comment.isNotEmpty; + } + + void submitComment() async { + String comment = commentController.text.trim(); + if (!isCommentValid()) { + return; // 直接返回,不显示提示 + } + + setState(() { + _isSendingComment = true; + }); + + try { + UniversalBean bean = await commentMusic().comment( + musicId: widget.id, + content: comment, + Authorization: AppData().currentToken, + ); + + if (bean.code == 200) { + commentController.clear(); + setState(() { + comments = [comment, ...comments]; + commentTimes = [ + formatDateTime(DateTime.now().toString()), + ...commentTimes + ]; + commentHeader = [avatar, ...commentHeader]; + commentName = [username, ...commentName]; + _total++; + }); + } + } catch (error) { + print('Error submitting comment: $error'); + } finally { + setState(() { + _isSendingComment = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + backgroundColor: const Color(0xffF6FFD1), + title: Text( + '评论($_total)', + style: TextStyle(color: Colors.black, fontSize: 22), + ), + elevation: 0, + leading: IconButton( + onPressed: () { + Get.back(); + }, + icon: Image.asset( + "assets/img/back.png", + width: 25, + height: 25, + fit: BoxFit.contain, + ), + ), + ), + body: Column( + children: [ + // ... 保持原有的顶部歌曲信息部分不变 ... + Container( + height: 80, + padding: const EdgeInsets.only(left: 20, right: 10), + decoration: BoxDecoration( + color: const Color(0xffF9F2AF), + borderRadius: BorderRadius.circular(20)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + widget.cover, + width: 60, + height: 60, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.song, + maxLines: 1, + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w400), + ), + Text( + widget.singer, + maxLines: 1, + style: const TextStyle( + color: Colors.black, fontSize: 14), + ) + ], + ), + ], + ), + ], + ), + ), + + // 评论区标题和排序 + Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "评论区", + style: TextStyle(fontSize: 18), + ), + Row( + children: [ + const Text( + "时间", + style: TextStyle(fontSize: 16), + ), + IconButton( + onPressed: () { + setState(() { + ascendingOrder = !ascendingOrder; + _sortComments(); + }); + }, + icon: Image.asset( + ascendingOrder + ? "assets/img/commend_up.png" + : "assets/img/commend_down.png", + fit: BoxFit.contain, + ), + ), + ], + ) + ], + ), + ), + + // 评论列表 + Expanded( + child: _isInitialLoading + ? const Center( + child: CircularProgressIndicator( + color: Color(0xff429482), + ), + ) + : ListView.builder( + controller: _scrollController, + itemCount: comments.length + 1, // +1 为了显示加载更多的提示 + itemBuilder: (context, index) { + if (index == comments.length) { + // 显示加载状态或"没有更多数据"的提示 + return Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.center, + child: _isLoading + ? const CircularProgressIndicator( + color: Color(0xff429482), + ) + : _hasMoreData + ? const Text('上拉加载更多') + : const Text('没有更多评论了'), + ); + } + + return ListTile( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + backgroundImage: + NetworkImage(commentHeader[index])), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(commentName[index], + style: const TextStyle(fontSize: 18)), + Text( + commentTimes[index], + style: const TextStyle(fontSize: 14), + ), + ], + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 50, top: 10, bottom: 20), + child: Text( + comments[index], + style: const TextStyle(fontSize: 18), + ), + ), + Container( + width: 560, + height: 2, + color: const Color(0xffE3F0ED), + ) + ], + ), + ); + }, + ), + ), + + // 评论输入框 + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: TextFieldColor( + controller: commentController, + hintText: '来发表你的评论吧!', + enabled: !_isSendingComment, + ), + ), + const SizedBox(width: 8.0), + ElevatedButton( + onPressed: isCommentValid() && !_isSendingComment + ? submitComment + : null, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xff429482), + foregroundColor: Colors.black45, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + minimumSize: const Size(30, 44), + disabledBackgroundColor: Colors.grey, + ), + child: _isSendingComment + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text( + '发送', + style: TextStyle(fontSize: 16), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/view/home_view.dart b/lib/view/home_view.dart index d61ec5b..aff5cc9 100644 --- a/lib/view/home_view.dart +++ b/lib/view/home_view.dart @@ -4,13 +4,16 @@ import 'package:get/get.dart'; import 'package:music_player_miao/api/api_music_return.dart'; import 'package:music_player_miao/common_widget/app_data.dart'; import 'package:music_player_miao/models/search_bean.dart'; -import 'package:music_player_miao/view/commend_view.dart'; +import 'package:music_player_miao/view/comment_view.dart'; import '../../view_model/home_view_model.dart'; +import '../api/api_music_likes.dart'; import '../api/api_music_list.dart'; import '../common/download_manager.dart'; import '../common_widget/Song_widegt.dart'; import '../common_widget/list_cell.dart'; +import '../models/MusicsListBean.dart'; import '../models/getMusicList_bean.dart'; +import '../models/universal_bean.dart'; import 'music_view.dart'; class HomeView extends StatefulWidget { @@ -20,67 +23,56 @@ class HomeView extends StatefulWidget { State createState() => _HomeViewState(); } -class _HomeViewState extends State { - // 使用 GetX 框架的依赖注入,将 HomeViewModel 实例注册为 homeVM。 - // Get.put() 方法会创建 HomeViewModel 的实例,并将其保存在 GetX 的依赖管理器中,方便后续使用。 +class _HomeViewState extends State + with AutomaticKeepAliveClientMixin { final homeVM = Get.put(HomeViewModel()); - - // 创建一个 TextEditingController,用于控制和监听搜索输入框的文本变化。 - // 这个控制器可以用于获取输入框的内容、清空输入框等操作。 final TextEditingController _controller = TextEditingController(); - - // 定义一个布尔变量 _isSearching,用于表示当前是否处于搜索状态。 - // 当用户在搜索框中输入内容时,_isSearching 会变为 true;当输入框为空时,_isSearching 会变为 false。 bool _isSearching = false; final downloadManager = Get.put(DownloadManager()); + List selectedSongs = []; + @override + bool get wantKeepAlive => true; + @override void initState() { super.initState(); _fetchSonglistData(); } - List selectedSongs = []; - Future _fetchSonglistData() async { - MusicListBean bean1 = - await GetMusic().getMusic1(Authorization: AppData().currentToken); - MusicListBean bean2 = - await GetMusic().getMusic2(Authorization: AppData().currentToken); - MusicListBean bean3 = - await GetMusic().getMusic3(Authorization: AppData().currentToken); - setState(() { - selectedSongs = [ - Song( - artistPic: bean1.coverPath!, - title: bean1.name!, - artist: bean1.singerName!, - musicurl: bean1.musicPath!, - pic: bean1.coverPath!, - id: bean1.id!, - likes: bean1.likeOrNot!, - collection: bean1.collectOrNot!), - Song( - artistPic: bean2.coverPath!, - title: bean2.name!, - artist: bean2.singerName!, - musicurl: bean2.musicPath!, - pic: bean2.coverPath!, - id: bean2.id!, - likes: bean2.likeOrNot!, - collection: bean2.collectOrNot!), - Song( - artistPic: bean3.coverPath!, - title: bean3.name!, - artist: bean3.singerName!, - musicurl: bean3.musicPath!, - pic: bean3.coverPath!, - id: bean3.id!, - likes: bean3.likeOrNot!, - collection: bean3.collectOrNot!), - ]; - }); + Future _onRefresh() async { + try { + // 重新获取数据 + await _fetchSonglistData(); + } catch (e) { + print('Refresh error: $e'); + // 可以在这里添加错误提示 + } } + Future _fetchSonglistData() async { + try { + MusicsListBean bean = + await GetMusic().getMusicList(Authorization: AppData().currentToken); + setState(() { + selectedSongs = []; + for (var data in bean.data!) { + selectedSongs.add(Song( + artistPic: data.coverPath!, + title: data.name!, + artist: data.singerName!, + musicurl: data.musicPath!, + pic: data.coverPath!, + id: data.id!, + likes: data.likeOrNot!, + collection: data.collectOrNot!, + )); + } + }); + } catch (e) { + print('Error occurred while fetching song list: $e'); + } + } ///轮播图 List imgList = [ @@ -107,32 +99,40 @@ class _HomeViewState extends State { // 循环处理每个搜索结果,通过 id 请求详细信息 for (var data in bean.data!) { - if (data.id != null) { // 确保 id 不为 null + if (data.id != null) { + // 确保 id 不为 null // 使用每个歌曲的 id 获取详细信息,并返回一个 Future - songDetailsFutures.add( - GetMusicDetail().getMusicDetail( - songId: data.id!, - Authorization: AppData().currentToken, - ).then((details) { - if (details != null) { - // 将详细歌曲信息封装成 Song 对象 - return Song( - artistPic: details.artistPic ?? '', // 歌手封面图 - title: data.name ?? '', // 歌曲名称 - artist: details.artist ?? '', // 歌手名称 - musicurl: details.musicurl ?? '', // 歌曲路径 - pic: details.pic ?? '', // 封面图片路径 - id: details.id, // 歌曲 ID - likes: details.likes, // 是否喜欢 - collection: details.collection, // 是否收藏 - ); - } - return null; // 如果没有详情返回 null - }).catchError((error) { - print("Error occurred while fetching song details: $error"); - return null; // 异常处理,返回 null - }) - ); + songDetailsFutures.add(GetMusicDetail() + .getMusicDetail( + songId: data.id!, + Authorization: AppData().currentToken, + ) + .then((details) { + if (details != null) { + // 将详细歌曲信息封装成 Song 对象 + return Song( + artistPic: details.artistPic ?? '', + // 歌手封面图 + title: data.name ?? '', + // 歌曲名称 + artist: details.artist ?? '', + // 歌手名称 + musicurl: details.musicurl ?? '', + // 歌曲路径 + pic: details.pic ?? '', + // 封面图片路径 + id: details.id, + // 歌曲 ID + likes: details.likes, + // 是否喜欢 + collection: details.collection, // 是否收藏 + ); + } + return null; // 如果没有详情返回 null + }).catchError((error) { + print("Error occurred while fetching song details: $error"); + return null; // 异常处理,返回 null + })); } else { print("Song ID is null for song: ${data.name}"); } @@ -142,12 +142,15 @@ class _HomeViewState extends State { List songDetailsList = await Future.wait(songDetailsFutures); // 过滤掉 null 值 - List validSongDetails = songDetailsList.where((song) => song != null).cast().toList(); + List validSongDetails = songDetailsList + .where((song) => song != null) + .cast() + .toList(); // 最后更新 UI,一次性更新 _filteredData setState(() { - _filteredData = validSongDetails; // 更新搜索结果 - _isSearching = true; // 设置正在搜索中 + _filteredData = validSongDetails; // 更新搜索结果 + _isSearching = true; // 设置正在搜索中 }); // 打印最终结果 @@ -175,6 +178,8 @@ class _HomeViewState extends State { @override Widget build(BuildContext context) { + super.build(context); + ///轮播图 var MySwiperWidget = Swiper( itemBuilder: (BuildContext context, int index) { @@ -205,222 +210,270 @@ class _HomeViewState extends State { child: Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.transparent, - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ///头部 - Container( - padding: const EdgeInsets.only(left: 20, top: 50), - child: const Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '喵听', - style: - TextStyle(fontSize: 35, fontWeight: FontWeight.bold), - ), - SizedBox( - width: 10, + body: Column( + children: [ + ///头部 + Container( + padding: const EdgeInsets.only(left: 20, top: 50), + child: const Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '喵听', + style: TextStyle(fontSize: 35, fontWeight: FontWeight.bold), + ), + SizedBox(width: 10), + Text( + '你的云端音乐库', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + ], + ), + ), + const SizedBox(height: 10), + + ///搜索 + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Column( + children: [ + Container( + height: 38, + decoration: BoxDecoration( + color: const Color(0xffF9F2AF), + borderRadius: BorderRadius.circular(19), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + offset: Offset(0, 1), + blurRadius: 0.1, + ) + ], ), - Text( - '你的云端音乐库', - style: - TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + child: TextField( + controller: _controller, + onChanged: (query) { + setState(() async { + _filterData(query); + }); + }, + decoration: InputDecoration( + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 20, + ), + prefixIcon: Container( + margin: const EdgeInsets.only(left: 20), + alignment: Alignment.centerLeft, + width: 30, + child: Image.asset( + "assets/img/home_search.png", + width: 20, + height: 20, + fit: BoxFit.contain, + ), + ), + hintText: "大家都在搜《背对背拥抱》", + hintStyle: const TextStyle( + color: Color(0xffA5A5A5), + fontSize: 13, + ), + ), ), - ], - ), - ), - const SizedBox(height: 10,), - ///搜索 - Container( - padding: const EdgeInsets.only(left: 20, right: 20, top: 10), - child: Column( - children: [ + ), + if (_isSearching) Container( - height: 38, + height: 150, + width: 345, decoration: BoxDecoration( - color: const Color(0xffF9F2AF), - borderRadius: BorderRadius.circular(19), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - offset: Offset(0, 1), - blurRadius: 0.1, - ) - ], + color: const Color(0xffF9F2AF).withOpacity(0.7), ), - child: TextField( - controller: _controller, - onChanged: (query) { - setState(() async { - _filterData(query); - }); + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: _filteredData.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(_filteredData[index].title), + onTap: () { + // 用户点击列表项时,执行以下操作: + Navigator.push( + // 使用 Navigator 进行页面跳转 + context, + MaterialPageRoute( + // 创建一个新的页面(MusicView),并将当前歌曲和索引作为参数传递给它 + builder: (context) => MusicView( + songList: _filteredData, + // 传递当前列表项对应的歌曲对象,包含歌曲的详细信息 + initialSongIndex: + index, // 传递当前歌曲在歌曲列表中的索引,用于在新页面中显示或操作 + ), + ), + ); + }, + ); }, - decoration: InputDecoration( - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 20, - ), - prefixIcon: Container( - margin: const EdgeInsets.only(left: 20), - alignment: Alignment.centerLeft, - width: 30, - child: Image.asset( - "assets/img/home_search.png", - width: 20, - height: 20, - fit: BoxFit.contain, - ), - ), - hintText: "大家都在搜《背对背拥抱》", - hintStyle: const TextStyle( - color: Color(0xffA5A5A5), - fontSize: 13, - ), - ), ), ), - if (_isSearching) + ], + ), + ), + + const SizedBox( + height: 10, + ), + + ///推荐+轮播图 + Expanded( + child: RefreshIndicator( + onRefresh: _onRefresh, + color: const Color(0xff429482), + backgroundColor: Colors.white, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ///推荐+轮播图 Container( - height: 150, - width: 345, - decoration: BoxDecoration( - color: const Color(0xffF9F2AF).withOpacity(0.7), - ), - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: _filteredData.length, - itemBuilder: (context, index) { - return ListTile( - title: Text(_filteredData[index].title), - onTap: () { - // 用户点击列表项时,执行以下操作: - Navigator.push( - // 使用 Navigator 进行页面跳转 - context, - MaterialPageRoute( - // 创建一个新的页面(MusicView),并将当前歌曲和索引作为参数传递给它 - builder: (context) => MusicView( - songList: _filteredData, // 传递当前列表项对应的歌曲对象,包含歌曲的详细信息 - initialSongIndex: index, // 传递当前歌曲在歌曲列表中的索引,用于在新页面中显示或操作 - ), - ), - ); - }, - ); - }, + padding: + const EdgeInsets.only(left: 20, right: 20, top: 10), + child: const Text( + '每日推荐', + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.w500), ), ), - ], - ), - ), + const SizedBox(height: 5), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 5), + height: 186, + width: double.infinity, + child: MySwiperWidget, + ), - const SizedBox(height: 10,), - ///推荐+轮播图 - Container( - padding: const EdgeInsets.only(left: 20, right: 20, top: 10), - child: const Text( - '每日推荐', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), - ), - ), - const SizedBox(height: 5,), - Container( - padding: const EdgeInsets.only(left: 20, right: 20, top: 5), - height: 186, - width: double.infinity, - child: MySwiperWidget, - ), + const SizedBox(height: 10), + + ///精选歌曲 + Container( + alignment: Alignment.topLeft, + padding: + const EdgeInsets.only(left: 20, right: 20, top: 5), + child: const Text( + '精选歌曲', + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.w500), + ), + ), + const SizedBox( + height: 5, + ), + ListView.builder( + padding: EdgeInsets.zero, + itemCount: selectedSongs.length, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + return ListTile( + leading: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + selectedSongs[index].pic, + width: 60, + height: 60, + fit: BoxFit.cover, + ), + ), + title: Text( + selectedSongs[index].title, + style: const TextStyle( + fontSize: 18, color: Colors.black), + ), + subtitle: Text( + selectedSongs[index].artist, + style: const TextStyle( + fontSize: 16, color: Colors.black), + ), + trailing: Padding( + padding: const EdgeInsets.only(right: 16), + child: InkWell( + onTap: () async { + setState(() { + selectedSongs[index].likes = + !selectedSongs[index].likes!; + }); - const SizedBox(height: 10), + UniversalBean response = await LikesApiMusic() + .likesMusic( + musicId: selectedSongs[index].id, + Authorization: + AppData().currentToken); - ///精选歌曲 - Container( - alignment: Alignment.topLeft, - padding: const EdgeInsets.only(left: 20, right: 20, top: 5), - child: const Text( - '精选歌曲', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), - ), - ), - const SizedBox(height: 5,), - ListView.builder( - padding: EdgeInsets.zero, - itemCount: selectedSongs.length, - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemBuilder: (context, index) { - return ListTile( - leading: Image.network(selectedSongs[index].pic), - title: Text( - selectedSongs[index].title, - style: const TextStyle(fontSize: 18, color: Colors.black), - ), - subtitle: Text( - selectedSongs[index].artist, - style: const TextStyle(fontSize: 16, color: Colors.black), - ), - trailing: InkWell( - onTap: () { - _bottomSheet(context, index); - }, - child: Image.asset('assets/img/More.png'), - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MusicView( - songList: selectedSongs, - initialSongIndex: index, - onSongStatusChanged: (index, isCollected, isLiked) { - setState(() { - // 更新父组件中的数据 - selectedSongs[index].collection = isCollected; - selectedSongs[index].likes = isLiked; - downloadManager.updateSongInfo(selectedSongs[index].id, isCollected, isLiked); - }); + if (response.code != 200) { + setState(() { + selectedSongs[index].likes = + !selectedSongs[index].likes!; + }); + } + }, + child: selectedSongs[index].likes! + ? Image.asset( + 'assets/img/like.png', + width: 24, + height: 24, + ) + : ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.grey[700]!, + BlendMode.srcIn, + ), + child: Image.asset( + 'assets/img/unlike.png', + width: 24, + height: 24, + ), + ), + ), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MusicView( + songList: selectedSongs, + initialSongIndex: index, + onSongStatusChanged: + (index, isCollected, isLiked) { + setState(() { + selectedSongs[index].collection = + isCollected; + selectedSongs[index].likes = isLiked; + downloadManager.updateSongInfo( + selectedSongs[index].id, + isCollected, + isLiked); + }); + }, + ), + ), + ); }, - ), - ), - ); - }, - ); - }, - ), + ); + }, + ), - const SizedBox(height: 10,), - ///精选歌单 - Container( - padding: const EdgeInsets.only(left: 20, right: 20, top: 5), - child: const Text( - '精选歌单', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + const SizedBox( + height: 10, + ), + ], + ), ), ), - const SizedBox(height: 5,), - SizedBox( - height: 180, - child: ListView.builder( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - padding: const EdgeInsets.only(left: 20), - itemCount: homeVM.listArr.length, - itemBuilder: (context, index) { - var sObj = homeVM.listArr[index]; - return ListRow( - sObj: sObj, - onPressed: () {}, - onPressedPlay: () {}, - ); - }), - ), - ], - ), + ), + ], ), ), ); @@ -433,7 +486,8 @@ class _HomeViewState extends State { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(30)), ), - builder: (context) => StatefulBuilder( // 使用StatefulBuilder以便动态修改状态 + builder: (context) => StatefulBuilder( + // 使用StatefulBuilder以便动态修改状态 builder: (context, setState) { bool likesnot = false; // 初始状态,假设未点赞 @@ -479,11 +533,11 @@ class _HomeViewState extends State { Column( children: [ IconButton( - onPressed: (){}, + onPressed: () {}, icon: Image.asset("assets/img/list_good.png"), iconSize: 60, ), - Text("点赞") + const Text("点赞") ], ), Column( @@ -491,9 +545,12 @@ class _HomeViewState extends State { IconButton( onPressed: () { Navigator.pop(context); - Get.to(() => CommentView( - initialSongIndex: index, - )); + // Get.to(() => + // CommentView( + // id:, + // song:, + // singer:, + // )); }, icon: Image.asset("assets/img/list_comment.png"), iconSize: 60, 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.dart b/lib/view/music_view.dart index e3a1bd5..b3b2b0e 100644 --- a/lib/view/music_view.dart +++ b/lib/view/music_view.dart @@ -6,9 +6,11 @@ import 'package:music_player_miao/common_widget/app_data.dart'; import '../../view_model/home_view_model.dart'; import '../api/api_music_likes.dart'; import '../api/api_collection.dart'; +import '../api/api_music_list.dart'; import '../common/download_manager.dart'; import '../common_widget/Song_widegt.dart'; -import '../view/commend_view.dart'; +import '../models/getMusicList_bean.dart'; +import '../view/comment_view.dart'; import '../models/universal_bean.dart'; class MusicView extends StatefulWidget { @@ -33,6 +35,7 @@ class _MusicViewState extends State with SingleTickerProviderStateMix AppData appData = AppData(); late int currentSongIndex; late AudioPlayer _audioPlayer; + StreamSubscription? _playerStateSubscription; final downloadManager = Get.put(DownloadManager()); @@ -62,31 +65,33 @@ class _MusicViewState extends State with SingleTickerProviderStateMix void initState() { super.initState(); currentSongIndex = widget.initialSongIndex; + // _initializeAsync(); + _fetchSonglistData(); + _updateCurrentSong(); playerInit(); - _initializeAsync(); _rotationController = AnimationController( duration: const Duration(seconds: 20), vsync: this, ); - // 2. 监听播放器状态来控制动画 - _audioPlayer.playerStateStream.listen((state) { - if (state.playing) { - // 当开始播放时,从当前角度继续旋转 - _rotationController.repeat(); - } else { - // 当暂停时,保持当前角度停止 - _rotationController.stop(canceled: false); + _playerStateSubscription = _audioPlayer.playerStateStream.listen((state) { + if (!_isDisposed) { + if (state.playing) { + _rotationController.repeat(); + } else { + _rotationController.stop(); + } } }); - } @override void dispose() { _isDisposed = true; - _audioPlayer.dispose(); + _playerStateSubscription?.cancel(); + _rotationController.stop(); _rotationController.dispose(); + _audioPlayer.dispose(); super.dispose(); } @@ -94,17 +99,60 @@ class _MusicViewState extends State with SingleTickerProviderStateMix setState(() { for (int i = 0; i < widget.songList.length; i++) { id.add(widget.songList[i].id); - song2.add(widget.songList[i].musicurl); + // TODO 处理为 musicurl 为空的情况, 需要通过网络 + if (widget.songList[i].musicurl == null) { + song2.add(""); + } else { + song2.add(widget.songList[i].musicurl!); + } artist.add(widget.songList[i].artist); music.add(widget.songList[i].title); - likes.add(widget.songList[i].likes); - collection.add(widget.songList[i].collection); + + // 初始化喜欢和收藏状态,后续再更新 + likes.add(widget.songList[i].likes ?? false); + collection.add(widget.songList[i].collection ?? false); } }); } + // 检查并更新歌曲状态的方法 + Future _checkAndUpdateSongStatus(int index) async { + // 只有当likes和collection为null时才需要请求 + if (widget.songList[index].likes == null || widget.songList[index].collection == null) { + try { + MusicListBean musicListBean = await GetMusic().getMusicById( + id: id[index], + Authorization: AppData().currentToken, + ); + + if (!_isDisposed && musicListBean.code == 200) { + setState(() { + likes[index] = musicListBean.likeOrNot!; + collection[index] = musicListBean.collectOrNot!; + // 如果是当前播放的歌曲,更新当前状态 + if (index == currentSongIndex) { + likesnot = musicListBean.likeOrNot!; + collectionsnot = musicListBean.collectOrNot!; + } + + widget.onSongStatusChanged?.call( + index, + musicListBean.collectOrNot!, + musicListBean.likeOrNot!, + ); + + }); + } + } catch (e) { + print('Error fetching song status: $e'); + } + } + } + + Future _updateCurrentSong() async { - // 立即更新UI和歌曲信息 + if (_isDisposed) return; + setState(() { _isLoading = true; _position = Duration.zero; @@ -115,22 +163,23 @@ class _MusicViewState extends State with SingleTickerProviderStateMix collectionsnot = collection[currentSongIndex]; }); + await _checkAndUpdateSongStatus(currentSongIndex); + try { - // 在后台停止当前播放 await _audioPlayer.stop(); _rotationController.reset(); - // Check for local file first + // 检查本地文件 final localSong = downloadManager.getLocalSong(currentSongIndex); final audioSource = localSong != null - ? AudioSource.file(localSong.musicurl) + ? AudioSource.file(localSong.musicurl!) : AudioSource.uri(Uri.parse(song2[currentSongIndex])); - // Set the audio source and get duration - final duration = await _audioPlayer.setAudioSource( - audioSource, - preload: true, - ); + // 设置音频源并获取时长 + await _audioPlayer.setAudioSource(audioSource, preload: true); + + // 等待获取真实的音频时长 + final duration = await _audioPlayer.duration; if (!_isDisposed) { setState(() { @@ -151,35 +200,86 @@ class _MusicViewState extends State with SingleTickerProviderStateMix } } - void playerInit() { + void playerInit() async { _audioPlayer = AudioPlayer(); - - // Initialize with first song + await _checkAndUpdateSongStatus(widget.initialSongIndex); + // 初始化第一首歌的信息 artistName = widget.songList[widget.initialSongIndex].artist; musicName = widget.songList[widget.initialSongIndex].title; - likesnot = widget.songList[widget.initialSongIndex].likes; - collectionsnot = widget.songList[widget.initialSongIndex].collection; + likesnot = widget.songList[widget.initialSongIndex].likes!; + collectionsnot = widget.songList[widget.initialSongIndex].collection!; - // Listen to player events + // 监听播放位置 _audioPlayer.positionStream.listen((position) { if (!_isDisposed) { setState(() => _position = position); } }); + // 监听音频时长 _audioPlayer.durationStream.listen((duration) { if (!_isDisposed) { setState(() => _duration = duration ?? Duration.zero); } }); - _audioPlayer.processingStateStream.listen((state) { - if (state == ProcessingState.completed) { + // 修改播放状态监听 + _audioPlayer.playerStateStream.listen((state) { + if (_isDisposed) return; + + if (state.processingState == ProcessingState.completed) { + // 在这里直接调用下一首,而不是通过 Stream + _handleSongCompletion(); + } + }); + } + + // 新增方法处理歌曲播放完成 + void _handleSongCompletion() { + if (_isDisposed) return; + + setState(() { + _position = Duration.zero; + _duration = Duration.zero; + }); + + // 使用 Future.microtask 确保状态更新后再切换歌曲 + Future.microtask(() { + if (!_isDisposed) { playNextSong(); } }); } + // 修改构建 Slider 的部分 + Widget _buildProgressSlider() { + // 确保最大值至少为 0.1 以避免除零错误 + final max = _duration.inSeconds.toDouble() == 0 ? 0.1 : _duration.inSeconds.toDouble(); + // 确保当前值不超过最大值 + final current = _position.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) async { + if (!_isDisposed && _duration.inSeconds > 0) { + await _audioPlayer.seek(Duration(seconds: value.toInt())); + setState(() {}); + } + }, + activeColor: const Color(0xff429482), + inactiveColor: const Color(0xffE3F0ED), + ), + ); + } + Widget _buildPlayButton() { return SizedBox( width: 52, @@ -216,14 +316,22 @@ class _MusicViewState extends State with SingleTickerProviderStateMix } void playOrPause() async { + if (_isDisposed) return; // 添加状态检查 + if (_audioPlayer.playing) { await _audioPlayer.pause(); - _rotationController.stop(canceled: false); + if (!_isDisposed) { // 再次检查状态 + _rotationController.stop(); + } } else { await _audioPlayer.play(); - _rotationController.repeat(); + if (!_isDisposed) { // 再次检查状态 + _rotationController.repeat(); + } + } + if (!_isDisposed) { + setState(() {}); } - setState(() {}); } void playNextSong() { @@ -468,7 +576,12 @@ class _MusicViewState extends State with SingleTickerProviderStateMix Navigator.push( context, MaterialPageRoute( - builder: (context) => CommentView(initialSongIndex: widget.initialSongIndex), + builder: (context) => CommentView( + id: id[currentSongIndex], + song: musicName, + singer: artistName, + cover: widget.songList[currentSongIndex].artistPic, + ), ), ); }, @@ -489,39 +602,15 @@ class _MusicViewState extends State with SingleTickerProviderStateMix mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "${formatDuration(_position)}", - style: const TextStyle( - color: Colors.black, - ), + formatDuration(_position), + style: const TextStyle(color: Colors.black), ), Expanded( - child: SliderTheme( - data: const SliderThemeData( - trackHeight: 3.0, // 调整轨道的高度 - thumbShape: RoundSliderThumbShape( - enabledThumbRadius: 7.0), // 调整拇指的大小 - overlayShape: - RoundSliderOverlayShape(overlayRadius: 12.0), - ), - child: Slider( - min: 0, - max: _duration.inSeconds.toDouble(), - value: _position.inSeconds.toDouble(), - onChanged: (value) async { - await _audioPlayer - .seek(Duration(seconds: value.toInt())); - setState(() {}); - }, - activeColor: const Color(0xff429482), - inactiveColor: const Color(0xffE3F0ED), - ), - ), + child: _buildProgressSlider(), ), Text( - "${formatDuration(_duration)}", - style: const TextStyle( - color: Colors.black, - ), + formatDuration(_duration), + style: const TextStyle(color: Colors.black), ), ], ), @@ -605,6 +694,9 @@ class _MusicViewState extends State with SingleTickerProviderStateMix ), ), ), + const SizedBox( + height: 10, + ), Expanded( child: ListView.builder( itemCount: music.length, @@ -641,21 +733,6 @@ class _MusicViewState extends State with SingleTickerProviderStateMix }, ), ), - ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xff429482), - padding: const EdgeInsets.symmetric(vertical: 14), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.zero, - ), - ), - child: const Text( - "关闭", - style: TextStyle(color: Colors.black, fontSize: 20), - ), - ), ], ), ); 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/rank_view.dart b/lib/view/rank_view.dart index b94bbf8..6c95fe6 100644 --- a/lib/view/rank_view.dart +++ b/lib/view/rank_view.dart @@ -15,7 +15,7 @@ class RankView extends StatefulWidget { State createState() => _RankViewState(); } -class _RankViewState extends State { +class _RankViewState extends State with AutomaticKeepAliveClientMixin { final rankVM = Get.put(RankViewModel()); List rankNames = []; List rankSingerName = []; @@ -25,42 +25,68 @@ class _RankViewState extends State { final downloadManager = Get.put(DownloadManager()); + @override + bool get wantKeepAlive => true; + @override void initState() { super.initState(); - _fetchSonglistData(); + _fetchTop50Data(); + } + + Future _onRefresh() async { + await _fetchTop50Data(); } - Future _fetchSonglistData() async { - RankBean bean2 = await GetRank().getRank(Authorization: AppData().currentToken); - setState(() { - rankNames = bean2.data!.map((data) => data.name!).toList(); - rankSingerName = bean2.data!.map((data) => data.singerName!).toList(); - rankCoverPath = bean2.data!.map((data) => data.coverPath!).toList(); - rankMusicPath = bean2.data!.map((data) => data.musicPath!).toList(); + Future _fetchTop50Data() async { + try { + RankBean bean2 = await GetRank().getRank(Authorization: AppData().currentToken); + if (bean2.code != 200) return; + rankNames.clear(); + rankSingerName.clear(); + rankCoverPath.clear(); + rankMusicPath.clear(); + + setState(() { + List ids = bean2.data!.map((data) => data.id!).toList(); + rankNames = bean2.data!.map((data) => data.name!).toList(); + rankSingerName = bean2.data!.map((data) => data.singerName!).toList(); + rankCoverPath = bean2.data!.map((data) => data.coverPath!).toList(); + rankMusicPath = bean2.data!.map((data) => data.musicPath!).toList(); - if (rankNames.isNotEmpty && - rankNames.length == rankSingerName.length && - rankNames.length == rankCoverPath.length && - rankNames.length == rankMusicPath.length) { - for (int i = 0; i < rankNames.length; i++) { - songs.add(Song( - artistPic: rankCoverPath[i], - title: rankNames[i], - artist: rankSingerName[i], - musicurl: rankMusicPath[i], - pic: rankCoverPath[i], - id: i, - likes: false, - collection: false, - )); + for (int i = 0; i < ids.length; i++) { + print(ids[i]); } - } - }); + + songs.clear(); + + if (rankNames.isNotEmpty && + rankNames.length == rankSingerName.length && + rankNames.length == rankCoverPath.length && + rankNames.length == rankMusicPath.length) { + for (int i = 0; i < rankNames.length; i++) { + songs.add(Song( + artistPic: rankCoverPath[i], + title: rankNames[i], + artist: rankSingerName[i], + musicurl: rankMusicPath[i], + pic: rankCoverPath[i], + id: ids[i], + likes: null, + collection: null, + )); + } + } + }); + } catch (e) { + // 可以添加错误处理逻辑 + print('Error fetching data: $e'); + } } @override Widget build(BuildContext context) { + super.build(context); return Container( decoration: const BoxDecoration( image: DecorationImage( @@ -74,20 +100,19 @@ class _RankViewState extends State { body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), //头部 const Center( child: Column( children: [ + SizedBox( + height: 10, + ), Text( '喵听排行榜', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w400), ), - SizedBox( - height: 10, - ), + SizedBox(height: 10), Text( 'Top50', style: TextStyle( @@ -95,19 +120,15 @@ class _RankViewState extends State { fontSize: 40, fontWeight: FontWeight.w500), ), - SizedBox( - height: 10, - ), - Text( - '2023/12/12更新 1期', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), - ), + // SizedBox(height: 10), + // Text( + // '2023/12/12更新 1期', + // style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + // ), ], ), ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), Container( decoration: const BoxDecoration( color: Colors.white, @@ -119,7 +140,25 @@ class _RankViewState extends State { child: Row( children: [ IconButton( - onPressed: () {}, + onPressed: () { + // 播放全部 + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MusicView( + songList: songs, + initialSongIndex: 0, + onSongStatusChanged: (index, isCollected, isLiked) { + setState(() { + songs[index].collection = isCollected; + songs[index].likes = isLiked; + downloadManager.updateSongInfo(songs[index].id, isCollected, isLiked); + }); + }, + ), + ), + ); + }, icon: Image.asset( "assets/img/button_play.png", width: 20, @@ -130,9 +169,7 @@ class _RankViewState extends State { '播放全部', style: TextStyle(fontSize: 16), ), - const SizedBox( - width: 5, - ), + const SizedBox(width: 5), const Text( '50', style: TextStyle(fontSize: 16), @@ -141,13 +178,16 @@ class _RankViewState extends State { ), ), Expanded( - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Container( - color: Colors.white, - child: Column( - children: [ - ListView.builder( + child: RefreshIndicator( + onRefresh: _onRefresh, + color: const Color(0xff429482), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), // 修改为始终可滚动 + child: Container( + color: Colors.white, + child: Column( + children: [ + ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, padding: const EdgeInsets.symmetric( @@ -156,119 +196,108 @@ class _RankViewState extends State { itemBuilder: (context, index) { int rankNum = index + 1; return ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MusicView( - songList: songs, - initialSongIndex: index, - onSongStatusChanged: (index, isCollected, isLiked) { - setState(() { - // 更新父组件中的数据 - songs[index].collection = isCollected; - songs[index].likes = isLiked; - downloadManager.updateSongInfo(songs[index].id, isCollected, isLiked); - }); - }, - ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MusicView( + songList: songs, + initialSongIndex: index, + onSongStatusChanged: (index, isCollected, isLiked) { + setState(() { + songs[index].collection = isCollected; + songs[index].likes = isLiked; + downloadManager.updateSongInfo(songs[index].id, isCollected, isLiked); + }); + }, ), - ); - }, - title: Column( - children: [ - Row( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - SizedBox( - width: 25, - child: RichText( - text: TextSpan( - text: rankNum.toString(), - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: Color(0xffCE0000), + ), + ); + }, + title: Column( + children: [ + Row( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 25, + child: RichText( + text: TextSpan( + text: rankNum.toString(), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Color(0xffCE0000), + ), ), ), ), - ), - const SizedBox( - width: 10, - ), - ClipRRect( - borderRadius: - BorderRadius.circular(10), - child: Image.network( - rankCoverPath[index], - width: 60, - height: 60, - fit: BoxFit.cover, - + const SizedBox(width: 10), + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + rankCoverPath[index], + width: 60, + height: 60, + fit: BoxFit.cover, + ), ), - ), - const SizedBox( - width: 20, - ), - SizedBox( - width: 170, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - rankNames[index], - maxLines: 1, - style: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: - FontWeight.w400), - ), - Text( - rankSingerName[index], - maxLines: 1, - style: const TextStyle( - color: Colors.black, - fontSize: 14), - ) - ], + const SizedBox(width: 20), + SizedBox( + width: 170, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + rankNames[index], + maxLines: 1, + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w400 + ), + ), + Text( + rankSingerName[index], + maxLines: 1, + style: const TextStyle( + color: Colors.black, + fontSize: 14 + ), + ) + ], + ), ), - ), - const SizedBox( - width: 18, - ), - ], - ), - IconButton( - onPressed: () { - _bottomSheet(context, index); // 传递当前的 index - }, - icon: Image.asset( - 'assets/img/More.png', - width: 25, - height: 25, - errorBuilder: (context, error, stackTrace) { - print('Error loading image: $error'); - return const Icon(Icons.error, size: 25); // 使用 const - }, + const SizedBox(width: 18), + ], ), - ), - const SizedBox( - height: 20, - ) - ], - ), - const SizedBox( - height: 10, - ) - ], - )); - }), - ], + // IconButton( + // onPressed: () { + // _bottomSheet(context, index); + // }, + // icon: Image.asset( + // 'assets/img/More.png', + // width: 25, + // height: 25, + // errorBuilder: (context, error, stackTrace) { + // print('Error loading image: $error'); + // return const Icon(Icons.error, size: 25); + // }, + // ), + // ), + + ], + ), + const SizedBox(height: 10) + ], + ), + ); + }, + ), + ], + ), ), ), ), @@ -278,12 +307,15 @@ class _RankViewState extends State { ), ); } - Future _bottomSheet(BuildContext context, int index){ + + Future _bottomSheet(BuildContext context, int index) { return showModalBottomSheet( context: context, backgroundColor: Colors.white, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(30))), - builder: (context) =>Container( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(30)) + ), + builder: (context) => Container( height: 150, padding: const EdgeInsets.only(top: 20), child: Column( @@ -349,7 +381,6 @@ class _RankViewState extends State { ], ), ) - ); } -} +} \ No newline at end of file diff --git a/lib/view/release_view.dart b/lib/view/release_view.dart index 5c13ae1..c22189f 100644 --- a/lib/view/release_view.dart +++ b/lib/view/release_view.dart @@ -24,7 +24,7 @@ class SongInfo { SongInfo({required this.songName, required this.artistName}); } -class _ReleaseViewState extends State { +class _ReleaseViewState extends State with AutomaticKeepAliveClientMixin { List coverImages = []; List songInfoList = []; @@ -35,8 +35,12 @@ class _ReleaseViewState extends State { bool isUploading = false; double uploadProgress = 0.0; + @override + bool get wantKeepAlive => true; + @override Widget build(BuildContext context) { + super.build(context); return Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.transparent, @@ -469,7 +473,7 @@ class _ReleaseViewState extends State { _showErrorMessage('请输入歌手名称'); return false; } - + // 检查文件大小 if (musicFile.lengthSync() > 10 * 1024 * 1024) { // 10MB 限制 _showErrorMessage('音乐文件大小不能超过10MB'); @@ -479,7 +483,7 @@ class _ReleaseViewState extends State { _showErrorMessage('封面图片大小不能超过2MB'); return false; } - + return true; } @@ -495,7 +499,7 @@ class _ReleaseViewState extends State { try { final dio = Dio(); - + // 设置拦截器来监听上传进度 dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { @@ -511,15 +515,15 @@ class _ReleaseViewState extends State { String coverFileName = path.basename(selectedCoverFile!.path); String musicFileName = path.basename(selectedMusicFile!.path); - + FormData formData = FormData.fromMap({ 'Authorization': AppData().currentToken, 'coverFile': await MultipartFile.fromFile( - selectedCoverFile!.path, + selectedCoverFile!.path, filename: coverFileName, ), 'musicFile': await MultipartFile.fromFile( - selectedMusicFile!.path, + selectedMusicFile!.path, filename: musicFileName, ), 'singerName': artistName, @@ -572,8 +576,8 @@ class _ReleaseViewState extends State { color: Color(0xff429482), fontSize: 14, ), - ), - ], + ], + ), ); } @@ -659,7 +663,7 @@ class _ReleaseViewState extends State { type: FileType.custom, allowedExtensions: ['mp3'], ); - + if (result != null && result.files.isNotEmpty) { return File(result.files.first.path!); } @@ -670,7 +674,7 @@ class _ReleaseViewState extends State { Future _pickImage() async { final picker = ImagePicker(); final pickedFile = await picker.pickImage(source: ImageSource.gallery); - + if (pickedFile != null) { return File(pickedFile.path); } diff --git a/lib/view/user/user_view.dart b/lib/view/user/user_view.dart index 08ad44b..b7fad99 100644 --- a/lib/view/user/user_view.dart +++ b/lib/view/user/user_view.dart @@ -24,7 +24,7 @@ class UserView extends StatefulWidget { State createState() => _UserViewState(); } -class _UserViewState extends State { +class _UserViewState extends State with AutomaticKeepAliveClientMixin { final homeVM = Get.put(HomeViewModel()); final TextEditingController _controller = TextEditingController(); int playlistCount = 0; @@ -36,6 +36,9 @@ class _UserViewState extends State { final downloadManager = Get.put(DownloadManager()); + @override + bool get wantKeepAlive => true; + @override void initState() { super.initState(); @@ -61,6 +64,7 @@ class _UserViewState extends State { @override Widget build(BuildContext context) { + super.build(context); return Container( decoration: const BoxDecoration( image: DecorationImage( @@ -86,10 +90,14 @@ class _UserViewState extends State { children: [ Row( children: [ - Image.network( - avatar, - width: 64, - height: 64, + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + avatar, + width: 60, + height: 60, + fit: BoxFit.cover, + ), ), const SizedBox( width: 25, @@ -123,7 +131,12 @@ class _UserViewState extends State { children: [ InkWell( onTap: () { - Get.to(const MyMusicView()); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const MyMusicView(), + ), + ); }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -159,7 +172,12 @@ class _UserViewState extends State { //我的收藏和本地下载分界 InkWell( onTap: () async { - final result = await Get.to(const MyDownloadView()); + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MyDownloadView(), + ), + ); if (result == true) { setState(() { @@ -252,7 +270,14 @@ class _UserViewState extends State { InkWell( onTap: () { print('点击成功'); - Get.to(MyMusicView(songlistIdd: playlistid[index])); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MyMusicView( + songlistIdd: playlistid[index] + ), + ), + ); }, child: Row( children: [ @@ -304,7 +329,12 @@ class _UserViewState extends State { children: [ InkWell( onTap: () { - Get.to(const MyWorkView()); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const MyWorkView(), + ), + ); }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -365,7 +395,12 @@ class _UserViewState extends State { IconButton( onPressed: () async { Navigator.pop(context); - bool result = await Get.to(const UserInfo()); + bool result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const UserInfo(), + ), + ); if (result) { setState(() { avatar = AppData().currentAvatar; @@ -388,6 +423,9 @@ class _UserViewState extends State { UniversalBean bean = await LogoutApiClient().logout( Authorization: AppData().currentToken, ); + AppData().currentToken = ''; + AppData().currentUsername = ''; + AppData().currentAvatar = ''; }, icon: Image.asset("assets/img/user_out.png"), iconSize: 60, diff --git a/lib/widget/text_field.dart b/lib/widget/text_field.dart index 9c8e58f..9b4a27d 100644 --- a/lib/widget/text_field.dart +++ b/lib/widget/text_field.dart @@ -8,6 +8,7 @@ class TextFieldColor extends StatelessWidget { final String? Function(String?)? validator; final FocusNode? focusNode; final String? Function(String?)? onChanged; + final bool enabled; // 添加enabled属性 const TextFieldColor({ super.key, @@ -18,11 +19,13 @@ class TextFieldColor extends StatelessWidget { this.validator, this.focusNode, this.onChanged, + this.enabled = true, // 设置默认值 }); @override Widget build(BuildContext context) { return TextFormField( + enabled: enabled, validator: validator, controller: controller, focusNode: focusNode, @@ -34,23 +37,26 @@ class TextFieldColor extends StatelessWidget { suffixIcon: suffixIcon, contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, ), - fillColor: Color(0xffE3F0ED), + disabledBorder: OutlineInputBorder( // 添加禁用状态的边框样式 + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + fillColor: enabled ? const Color(0xffE3F0ED) : const Color(0xffE3F0ED).withOpacity(0.7), // 禁用时稍微调整透明度 filled: true, hintText: hintText, alignLabelWithHint: true, hintStyle: TextStyle( - color: Color(0xff6E6E6E), - fontSize: 16 + color: enabled ? const Color(0xff6E6E6E) : const Color(0xff6E6E6E).withOpacity(0.5), + fontSize: 16, ), ), ); } - } \ No newline at end of file