diff --git a/android/app/.gradle/config.properties b/android/app/.gradle/config.properties new file mode 100644 index 0000000..ba5fd20 --- /dev/null +++ b/android/app/.gradle/config.properties @@ -0,0 +1,2 @@ +#Mon Oct 28 21:45:22 CST 2024 +java.home=C\:\\Program Files\\Android\\Android Studio\\jbr diff --git a/android/app/local.properties b/android/app/local.properties new file mode 100644 index 0000000..3a34857 --- /dev/null +++ b/android/app/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Mon Oct 28 21:45:22 CST 2024 +sdk.dir=C\:\\Users\\zxp\\AppData\\Local\\Android\\Sdk diff --git a/android/gradle.properties b/android/gradle.properties index 94adc3a..fb29d12 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +systemProp.org.gradle.wrapper.timeout=300000 diff --git a/lib/api/api_client_info.dart b/lib/api/api_client_info.dart index 24c3f08..7303570 100644 --- a/lib/api/api_client_info.dart +++ b/lib/api/api_client_info.dart @@ -1,7 +1,6 @@ import 'dart:io'; - import 'package:dio/dio.dart'; - +import 'package:flutter/material.dart'; import '../models/universal_bean.dart'; const String _changeNameURL = 'http://flyingpig.fun:10010/users/username'; @@ -9,6 +8,7 @@ const String _changeHeaderURL = 'http://flyingpig.fun:10010/users/avatar'; class ChangeApiClient { final Dio dio = Dio(); + final ValueNotifier avatarUrlNotifier = ValueNotifier(""); ///修改昵称 Future changeName({ required String Authorization, @@ -16,13 +16,19 @@ class ChangeApiClient { }) async { Response response = await dio.put( _changeNameURL, - data: { - 'Authorization': Authorization, - }, + // data: { + // 'Authorization': Authorization, + // }, queryParameters: {'userName':userName}, - options: Options(headers:{'Authorization':Authorization,'Content-Type':'application/json;charset=UTF-8'}) + options: Options( + headers:{ + 'Authorization':Authorization, + 'Content-Type':'application/json;charset=UTF-8' + } + ) ); print(response.data); + return UniversalBean.formMap(response.data); } ///修改头像 @@ -32,7 +38,7 @@ class ChangeApiClient { required File avatar, }) async { FormData formData = FormData.fromMap({ - 'Authorization': Authorization, + // 'Authorization': Authorization, 'avatar': await MultipartFile.fromFile(avatar.path, filename: 'avatar.jpg'), }); @@ -46,8 +52,8 @@ class ChangeApiClient { }, ), ); - print(response.data); + avatarUrlNotifier.value = avatar.path; return UniversalBean.formMap(response.data); } diff --git a/lib/api/api_music_list.dart b/lib/api/api_music_list.dart index 37eb2a9..ab6c26e 100644 --- a/lib/api/api_music_list.dart +++ b/lib/api/api_music_list.dart @@ -1,13 +1,15 @@ import 'package:dio/dio.dart'; +import '../common_widget/Song_widegt.dart'; import '../models/getMusicList_bean.dart'; import '../models/getRank_bean.dart'; const String _getMusic1 = 'http://flyingpig.fun:10010/musics/1'; const String _getMusic2 = 'http://flyingpig.fun:10010/musics/2'; const String _getMusic3 = 'http://flyingpig.fun:10010/musics/3'; +const String _getSongDetail = 'http://flyingpig.fun:10010/musics'; -///排行榜 +/// 精选歌曲 class GetMusic { final Dio dio = Dio(); @@ -51,3 +53,40 @@ class GetMusic { return MusicListBean.formMap(response.data); } } + +// 获取歌曲详细信息 +class GetMusicDetail { + final Dio dio = Dio(); + + // 根据歌曲ID获取歌曲详情 + Future getMusicDetail({required int songId, required String Authorization}) async { + try { + // 更新路径,将 songId 作为路径参数插入 URL + final String url = 'http://flyingpig.fun:10010/musics/$songId'; // 假设这个是你的API路径 + + // 发起 GET 请求 + Response response = await dio.get( + url, + options: Options( + headers: { + 'Authorization': Authorization, + 'Content-Type': 'application/json;charset=UTF-8', + }, + ), + ); + + print("Song detail response: ${response.data}"); + + // 检查响应的状态码和数据 + if (response.statusCode == 200) { + // 将返回的响应数据封装成Song对象 + return Song.fromMap(response.data['data']); + } else { + throw Exception("Failed to load song details"); + } + } catch (e) { + print("Error occurred while fetching song details: $e"); + rethrow; // 抛出异常以供调用方处理 + } + } +} diff --git a/lib/api/api_music_return.dart b/lib/api/api_music_return.dart index ad2ffcf..1905401 100644 --- a/lib/api/api_music_return.dart +++ b/lib/api/api_music_return.dart @@ -9,18 +9,27 @@ import '../models/search_bean.dart'; const String _SearchURL = 'http://flyingpig.fun:10010/musics/search'; const String _postComment = 'http://flyingpig.fun:10010/comments'; ///搜索 -class SearchMusic{ +class SearchMusic { final Dio dio = Dio(); - Future search({required String keyword,}) async { + + Future search({ + required String keyword, + required String Authorization, + }) async { Response response = await dio.get( - _SearchURL, - queryParameters: {'keyword':keyword} + _SearchURL, + queryParameters: {'keyword': keyword}, + options: Options(headers: { + 'Authorization': Authorization, + 'Content-Type': 'application/json;charset=UTF-8', + }), ); print(response.data); return SearchBean.formMap(response.data); } } + ///评论 class commentMusic { final Dio dio = Dio(); diff --git a/lib/common_widget/Song_widegt.dart b/lib/common_widget/Song_widegt.dart index b59cb08..1b3b92e 100644 --- a/lib/common_widget/Song_widegt.dart +++ b/lib/common_widget/Song_widegt.dart @@ -8,13 +8,29 @@ class Song { bool likes; bool collection; - Song( - {required this.pic, - required this.artistPic, - required this.title, - required this.artist, - required this.musicurl, - required this.id, - required this.likes, - required this.collection}); + // 构造函数 + Song({ + required this.pic, + required this.artistPic, + required this.title, + required this.artist, + required this.musicurl, + required this.id, + required this.likes, + required this.collection, + }); + + // 使用 Map 数据创建 Song 实例 + factory Song.fromMap(Map map) { + return Song( + pic: map['coverPath'] ?? '', // 封面图,假设是 coverPath + artistPic: map['coverPath'] ?? '', // 如果没有返回值或字段,可以为空字符串(示例中没有提供 artistPic) + title: map['name'] ?? '', // 歌曲名称,假设是 name + artist: map['singerName'] ?? '', // 歌手名称,假设是 singerName + musicurl: map['musicPath'] ?? '', // 歌曲路径,假设是 musicPath + id: map['id'] ?? 0, // 歌曲 ID,假设是 id + likes: map['likeOrNot'] ?? false, // 是否喜欢,假设是 likeOrNot + collection: map['collectOrNot'] ?? false, // 是否收藏,假设是 collectOrNot + ); + } } diff --git a/lib/view/home_view.dart b/lib/view/home_view.dart index f8f54f6..dde9209 100644 --- a/lib/view/home_view.dart +++ b/lib/view/home_view.dart @@ -21,17 +21,25 @@ class HomeView extends StatefulWidget { } class _HomeViewState extends State { + // 使用 GetX 框架的依赖注入,将 HomeViewModel 实例注册为 homeVM。 + // Get.put() 方法会创建 HomeViewModel 的实例,并将其保存在 GetX 的依赖管理器中,方便后续使用。 final homeVM = Get.put(HomeViewModel()); + + // 创建一个 TextEditingController,用于控制和监听搜索输入框的文本变化。 + // 这个控制器可以用于获取输入框的内容、清空输入框等操作。 final TextEditingController _controller = TextEditingController(); + + // 定义一个布尔变量 _isSearching,用于表示当前是否处于搜索状态。 + // 当用户在搜索框中输入内容时,_isSearching 会变为 true;当输入框为空时,_isSearching 会变为 false。 bool _isSearching = false; final downloadManager = Get.put(DownloadManager()); + void initState() { super.initState(); _fetchSonglistData(); } - List songs = []; - + List selectedSongs = []; Future _fetchSonglistData() async { MusicListBean bean1 = await GetMusic().getMusic1(Authorization: AppData().currentToken); @@ -41,7 +49,7 @@ class _HomeViewState extends State { await GetMusic().getMusic3(Authorization: AppData().currentToken); setState(() { - songs = [ + selectedSongs = [ Song( artistPic: bean1.coverPath!, title: bean1.name!, @@ -81,23 +89,85 @@ class _HomeViewState extends State { {"image": "assets/img/banner.png"}, ]; - List _filteredData = []; + List _filteredData = []; Future _filterData(String query) async { if (query.isNotEmpty) { - SearchBean bean = await SearchMusic().search(keyword: query); - if (bean.code == 200) { + try { + // 发起搜索请求 + SearchBean bean = await SearchMusic().search( + keyword: query, + Authorization: AppData().currentToken, + ); + + // 如果请求成功且返回的数据不为空 + if (bean.code == 200 && bean.data != null) { + // 创建一个临时列表来存储所有异步请求 + List> songDetailsFutures = []; + + // 循环处理每个搜索结果,通过 id 请求详细信息 + for (var data in bean.data!) { + 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 + }) + ); + } else { + print("Song ID is null for song: ${data.name}"); + } + } + + // 使用 Future.wait 等待所有异步请求完成 + List songDetailsList = await Future.wait(songDetailsFutures); + + // 过滤掉 null 值 + List validSongDetails = songDetailsList.where((song) => song != null).cast().toList(); + + // 最后更新 UI,一次性更新 _filteredData + setState(() { + _filteredData = validSongDetails; // 更新搜索结果 + _isSearching = true; // 设置正在搜索中 + }); + + // 打印最终结果 + print("Filtered Data: $_filteredData"); + } else { + setState(() { + _filteredData = []; + _isSearching = false; + }); + } + } catch (error) { + print("Error occurred during search: $error"); setState(() { - _filteredData = bean.data - ?.map((data) => - "${data.name} ") // Adjust this based on your data structure - .toList() ?? - []; - _isSearching = true; + _filteredData = []; + _isSearching = false; }); } } else { setState(() { + _filteredData = []; _isSearching = false; }); } @@ -226,7 +296,21 @@ class _HomeViewState extends State { itemCount: _filteredData.length, itemBuilder: (context, index) { return ListTile( - title: Text(_filteredData[index]), + title: Text(_filteredData[index].title), + onTap: () { + // 用户点击列表项时,执行以下操作: + Navigator.push( + // 使用 Navigator 进行页面跳转 + context, + MaterialPageRoute( + // 创建一个新的页面(MusicView),并将当前歌曲和索引作为参数传递给它 + builder: (context) => MusicView( + songList: _filteredData, // 传递当前列表项对应的歌曲对象,包含歌曲的详细信息 + initialSongIndex: index, // 传递当前歌曲在歌曲列表中的索引,用于在新页面中显示或操作 + ), + ), + ); + }, ); }, ), @@ -261,18 +345,18 @@ class _HomeViewState extends State { ), ListView.builder( padding: EdgeInsets.zero, - itemCount: songs.length, + itemCount: selectedSongs.length, physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemBuilder: (context, index) { return ListTile( - leading: Image.network(songs[index].pic), + leading: Image.network(selectedSongs[index].pic), title: Text( - songs[index].title, + selectedSongs[index].title, style: const TextStyle(fontSize: 18, color: Colors.black), ), subtitle: Text( - songs[index].artist, + selectedSongs[index].artist, style: const TextStyle(fontSize: 16, color: Colors.black), ), trailing: InkWell( @@ -286,14 +370,14 @@ class _HomeViewState extends State { context, MaterialPageRoute( builder: (context) => MusicView( - songList: songs, + songList: selectedSongs, initialSongIndex: index, onSongStatusChanged: (index, isCollected, isLiked) { setState(() { // 更新父组件中的数据 - songs[index].collection = isCollected; - songs[index].likes = isLiked; - downloadManager.updateSongInfo(songs[index].id, isCollected, isLiked); + selectedSongs[index].collection = isCollected; + selectedSongs[index].likes = isLiked; + downloadManager.updateSongInfo(selectedSongs[index].id, isCollected, isLiked); }); }, ), diff --git a/lib/view/user/user_info.dart b/lib/view/user/user_info.dart index 65650fb..431c246 100644 --- a/lib/view/user/user_info.dart +++ b/lib/view/user/user_info.dart @@ -1,17 +1,12 @@ -// ignore_for_file: use_build_context_synchronously - import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:music_player_miao/widget/text_field.dart'; +import 'package:image_picker/image_picker.dart'; import '../../api/api_client.dart'; import '../../api/api_client_info.dart'; import '../../common_widget/app_data.dart'; -import '../../models/getInfo_bean.dart'; -import '../../models/universal_bean.dart'; import '../../view_model/home_view_model.dart'; -import 'package:image_picker/image_picker.dart'; +import '../../view/main_tab_view/main_tab_view.dart'; class UserInfo extends StatefulWidget { const UserInfo({super.key}); @@ -23,7 +18,7 @@ class UserInfo extends StatefulWidget { class _UserInfoState extends State { final listVM = Get.put(HomeViewModel()); final TextEditingController _controller = TextEditingController(); - late File _selectedImage; + File? _selectedImage; @override Widget build(BuildContext context) { @@ -42,7 +37,17 @@ class _UserInfoState extends State { elevation: 0, leading: IconButton( onPressed: () { - Get.back(); + // 返回到 MainTabView 并切换到“个人”标签页 + Get.back(); // 关闭当前页面并返回到主界面 + + // 等待页面关闭后再切换标签 + Future.delayed(Duration(milliseconds: 100), () { + // 查找 MainTabView 并获取 TabController + final mainTabController = Get.find().getController(context); + if (mainTabController != null) { + mainTabController.index = 3; // 切换到“个人”标签页的索引 + } + }); }, icon: Image.asset( "assets/img/back.png", @@ -60,77 +65,8 @@ class _UserInfoState extends State { body: SingleChildScrollView( child: Column( children: [ - Container( - height: 80, - color: Colors.white.withOpacity(0.6), - padding: const EdgeInsets.only(left: 48, right: 25), - child: InkWell( - onTap: () { - _bottomSheet(context); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "头像", - style: TextStyle(fontSize: 20), - ), - Row( - children: [ - Image.network( - AppData().currentAvatar, - width: 64, - height: 64, - ), - const SizedBox( - width: 20, - ), - Image.asset( - "assets/img/user_next.png", - width: 25, - height: 25, - ) - ], - ) - ], - ), - ), - ), - Container( - height: 80, - color: Colors.white.withOpacity(0.6), - padding: const EdgeInsets.only(left: 48, right: 25), - child: InkWell( - onTap: () { - _showNicknameDialog(); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "昵称", - style: TextStyle(fontSize: 20), - ), - Row( - children: [ - Text( - AppData().currentUsername, - style: const TextStyle(fontSize: 20), - ), - const SizedBox( - width: 15, - ), - Image.asset( - "assets/img/user_next.png", - width: 25, - height: 25, - ) - ], - ) - ], - ), - ), - ) + _buildAvatarRow(), + _buildNicknameRow(), ], ), ), @@ -138,68 +74,159 @@ class _UserInfoState extends State { ); } + // 构建头像行 + Widget _buildAvatarRow() { + return Container( + height: 80, + color: Colors.white.withOpacity(0.6), + padding: const EdgeInsets.only(left: 48, right: 25), + child: InkWell( + onTap: () { + _bottomSheet(context); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "头像", + style: TextStyle(fontSize: 20), + ), + Row( + children: [ + _buildAvatarImage(), + const SizedBox(width: 20), + Image.asset( + "assets/img/user_next.png", + width: 25, + height: 25, + ), + ], + ), + ], + ), + ), + ); + } + + // 构建昵称行 + Widget _buildNicknameRow() { + return Container( + height: 80, + color: Colors.white.withOpacity(0.6), + padding: const EdgeInsets.only(left: 48, right: 25), + child: InkWell( + onTap: () { + _showNicknameDialog(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "昵称", + style: TextStyle(fontSize: 20), + ), + Row( + children: [ + Text( + AppData().currentUsername, + style: const TextStyle(fontSize: 20), + ), + const SizedBox(width: 15), + Image.asset( + "assets/img/user_next.png", + width: 25, + height: 25, + ), + ], + ), + ], + ), + ), + ); + } + + // 显示头像的 Widget,根据是否是网络 URL 或本地路径来动态加载 + Widget _buildAvatarImage() { + final avatarPath = AppData().currentAvatar; + if (avatarPath.startsWith('http')) { + return Image.network( + avatarPath, + width: 64, + height: 64, + ); + } else { + return Image.file( + File(avatarPath), + width: 64, + height: 64, + ); + } + } + Future _bottomSheet(BuildContext context) async { final picker = ImagePicker(); await showModalBottomSheet( - context: context, - backgroundColor: Colors.white, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(30))), - builder: (context) => Container( - height: 132, - padding: const EdgeInsets.only(top: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ElevatedButton( - onPressed: () async { - Navigator.pop(context); - final pickedFile = - await picker.pickImage(source: ImageSource.gallery); - if (pickedFile != null) { - _selectedImage=File('assets/images/bg.png'); - setState(() { - _selectedImage = File(pickedFile.path); - print(_selectedImage); - }); - UniversalBean bean = await ChangeApiClient().changeHeader( - Authorization: AppData().currentToken, - avatar: _selectedImage); - GetInfoBean bean1 = await GetInfoApiClient().getInfo( - Authorization: AppData().currentToken); - AppData appData = AppData(); - appData.box.write('currentAvatar', AppData().currentAvatar); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 12), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: const Text( - "从相册上传头像", - style: TextStyle(color: Colors.black, fontSize: 18), - ), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xff429482), - padding: const EdgeInsets.symmetric(vertical: 18), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.zero, - ), - ), - child: const Text( - "取消", - style: TextStyle(color: Colors.black, fontSize: 18), - ), - ), - ], + context: context, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(30))), + builder: (context) => Container( + height: 132, + padding: const EdgeInsets.only(top: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + onPressed: () async { + Navigator.pop(context); + final pickedFile = + await picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + _selectedImage = File(pickedFile.path); + setState(() {}); // 更新 UI + + // 上传头像 + await ChangeApiClient().changeHeader( + Authorization: AppData().currentToken, + avatar: _selectedImage!); + + // 更新本地存储 + _updatetouxiang(_selectedImage!.path); + // 拉取更新后的用户信息 + await GetInfoApiClient().getInfo( + Authorization: AppData().currentToken); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 12), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), - )); + child: const Text( + "从相册上传头像", + style: TextStyle(color: Colors.black, fontSize: 18), + ), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xff429482), + padding: const EdgeInsets.symmetric(vertical: 18), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.zero, + ), + ), + child: const Text( + "取消", + style: TextStyle(color: Colors.black, fontSize: 18), + ), + ), + ], + ), + ), + ); } void _showNicknameDialog() { @@ -209,12 +236,12 @@ class _UserInfoState extends State { return AlertDialog( title: const Center( child: Text( - "修改昵称", - style: TextStyle(fontSize: 20), - )), - content: TextFieldColor( + "修改昵称", + style: TextStyle(fontSize: 20), + )), + content: TextField( controller: _controller, - hintText: '请输入新昵称', + decoration: const InputDecoration(hintText: '请输入新昵称'), ), actions: [ TextButton( @@ -225,8 +252,7 @@ class _UserInfoState extends State { backgroundColor: const Color(0xff429482), minimumSize: const Size(130, 50), shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(5.0), // Adjust the radius as needed + borderRadius: BorderRadius.circular(5.0), ), ), child: const Text( @@ -237,7 +263,7 @@ class _UserInfoState extends State { TextButton( onPressed: () async { _updateNickname(); - UniversalBean bean = await ChangeApiClient().changeName( + await ChangeApiClient().changeName( Authorization: AppData().currentToken, userName: AppData().currentUsername); Navigator.of(context).pop(); @@ -246,8 +272,7 @@ class _UserInfoState extends State { backgroundColor: const Color(0xff429482), minimumSize: const Size(130, 50), shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(5.0), // Adjust the radius as needed + borderRadius: BorderRadius.circular(5.0), ), ), child: const Text( @@ -267,4 +292,14 @@ class _UserInfoState extends State { appData.box.write('currentUsername', _controller.text); }); } + + void _updatetouxiang(String path) { + setState(() { + AppData appData = AppData(); + appData.box.write('currentAvatar', path); // 更新头像路径到本地存储 + }); + } } + + + diff --git a/pubspec.yaml b/pubspec.yaml index 5967f77..b0b9886 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,7 @@ environment: dependencies: flutter: sdk: flutter - win32: ^5.1.0 + win32: ^5.7.1 flutter_swiper_view: 1.1.8 audioplayers: ^5.2.1 permission_handler: ^11.0.1