import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; import 'package:yumi/modules/room/music/room_music_folder_page.dart'; import 'package:yumi/modules/room/music/room_music_texts.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/music/local_music_scanner.dart'; import 'package:yumi/services/music/room_music_manager.dart'; import 'package:yumi/shared/data_sources/models/sc_music_mode.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/ui_kit/widgets/room/music/room_music_player_bar.dart'; class RoomMusicPage extends StatefulWidget { const RoomMusicPage({super.key}); @override State createState() => _RoomMusicPageState(); } class _RoomMusicPageState extends State { final TextEditingController _searchController = TextEditingController(); final LocalMusicScanner _scanner = LocalMusicScanner(); bool _isScanning = false; bool _isEditing = false; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) { return; } final manager = context.read(); manager.attachRtcProvider(context.read()); manager.reload(); }); } @override void dispose() { _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF071F1B), body: SafeArea( child: Stack( children: [ Column( children: [ _buildHeader(context), _buildSearchField(), Expanded( child: Consumer( builder: (context, manager, child) { final songs = _filteredSongs(manager.playlist); if (manager.playlist.isEmpty) { return _buildEmptyState(context); } return Column( children: [ _buildTotal(context, manager.playlist.length), Expanded( child: _isEditing && _searchController.text.isEmpty ? _buildReorderableList(manager, songs) : _buildSongList(manager, songs), ), ], ); }, ), ), ], ), Positioned( left: 0, right: 0, bottom: 0, child: const RoomMusicPlayerBar(), ), if (_isScanning) Positioned.fill( child: Container( color: Colors.black.withValues(alpha: 0.35), child: const Center( child: CircularProgressIndicator(color: Color(0xFF33E6A2)), ), ), ), ], ), ), ); } Widget _buildHeader(BuildContext context) { return SizedBox( height: 54.w, child: Row( children: [ IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white), ), Expanded( child: Text( RoomMusicTexts.t(context, "music", "音乐"), textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 18.sp, fontWeight: FontWeight.w800, ), ), ), IconButton( onPressed: _startAddFlow, icon: Image.asset( "sc_images/room/sc_icon_room_music_add.png", width: 24.w, height: 24.w, ), ), IconButton( onPressed: () { setState(() { _isEditing = !_isEditing; }); }, icon: Image.asset( "sc_images/room/sc_music_material_edit.png", width: 24.w, height: 24.w, ), ), ], ), ); } Widget _buildSearchField() { return Padding( padding: EdgeInsets.fromLTRB(16.w, 4.w, 16.w, 10.w), child: TextField( controller: _searchController, onChanged: (_) => setState(() {}), style: TextStyle(color: Colors.white, fontSize: 13.sp), decoration: InputDecoration( isDense: true, hintText: RoomMusicTexts.t(context, "search", "搜索"), hintStyle: TextStyle( color: Colors.white.withValues(alpha: 0.42), fontSize: 13.sp, ), prefixIcon: Icon( Icons.search, color: Colors.white.withValues(alpha: 0.42), size: 20.w, ), filled: true, fillColor: Colors.white.withValues(alpha: 0.08), border: OutlineInputBorder( borderRadius: BorderRadius.circular(18.w), borderSide: BorderSide.none, ), ), ), ); } Widget _buildTotal(BuildContext context, int count) { final template = RoomMusicTexts.t( context, "roomMusicTotalSongs", "总计:{1} 首歌曲", ); return Padding( padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 8.w), child: Align( alignment: AlignmentDirectional.centerStart, child: Text( template.replaceAll("{1}", count.toString()), style: TextStyle( color: Colors.white.withValues(alpha: 0.62), fontSize: 12.sp, ), ), ), ); } Widget _buildEmptyState(BuildContext context) { return Center( child: Padding( padding: EdgeInsets.only(bottom: 120.w), child: Column( mainAxisSize: MainAxisSize.min, children: [ Image.asset( "sc_images/room/sc_icon_room_music_empty.png", width: 86.w, height: 86.w, ), SizedBox(height: 12.w), Text( RoomMusicTexts.t(context, "roomMusicEmpty", "没有音乐"), style: TextStyle( color: Colors.white.withValues(alpha: 0.72), fontSize: 14.sp, fontWeight: FontWeight.w600, ), ), SizedBox(height: 18.w), ElevatedButton( onPressed: _startAddFlow, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF33E6A2), foregroundColor: const Color(0xFF06251F), minimumSize: Size(164.w, 42.w), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(22.w), ), ), child: Text( RoomMusicTexts.t(context, "roomMusicScanFromPhone", "从手机扫描添加"), style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w700), ), ), ], ), ), ); } Widget _buildSongList(RoomMusicManager manager, List songs) { final bottomPadding = manager.current == null ? 20.w : 190.w; return ListView.builder( padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, bottomPadding), itemCount: songs.length, itemBuilder: (context, index) { final item = songs[index]; return Dismissible( key: ValueKey("music-${item.id}"), direction: DismissDirection.endToStart, confirmDismiss: (_) => _confirmDelete(item), background: Container( alignment: AlignmentDirectional.centerEnd, padding: EdgeInsetsDirectional.only(end: 18.w), color: const Color(0xFFE64A4A), child: Image.asset( "sc_images/room/sc_music_material_delete.png", width: 24.w, height: 24.w, ), ), child: _SongTile( item: item, selected: manager.current?.id == item.id, playing: manager.current?.id == item.id && manager.isPlaying, onTap: () => manager.play(item), trailing: _isEditing ? Image.asset( "sc_images/room/sc_music_material_more.png", width: 24.w, height: 24.w, ) : null, ), ); }, ); } Widget _buildReorderableList( RoomMusicManager manager, List songs, ) { final bottomPadding = manager.current == null ? 20.w : 190.w; return ReorderableListView.builder( padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, bottomPadding), itemCount: songs.length, onReorder: (oldIndex, newIndex) { manager.reorder(oldIndex, newIndex); }, itemBuilder: (context, index) { final item = songs[index]; return _SongTile( key: ValueKey("reorder-${item.id}"), item: item, selected: manager.current?.id == item.id, playing: manager.current?.id == item.id && manager.isPlaying, onTap: () => manager.play(item), trailing: ReorderableDragStartListener( index: index, child: Image.asset( "sc_images/room/sc_music_material_more.png", width: 24.w, height: 24.w, ), ), ); }, ); } Future _startAddFlow() async { if (_isScanning) { return; } setState(() { _isScanning = true; }); final manager = context.read(); final folders = await _scanner.scanMp3Folders(addedSongs: manager.playlist); if (!mounted) { return; } setState(() { _isScanning = false; }); final songCount = folders.fold( 0, (sum, item) => sum + item.musicCount, ); if (songCount == 0) { SCTts.show(RoomMusicTexts.t(context, "roomMusicNotFound", "未找到音乐")); return; } final changed = await Navigator.of(context).push( MaterialPageRoute(builder: (_) => RoomMusicFolderPage(folders: folders)), ); if (changed == true && mounted) { await manager.reload(); } } Future _confirmDelete(SCMusicMode item) async { final result = await showDialog( context: context, builder: (dialogContext) { return AlertDialog( backgroundColor: const Color(0xFF10352E), title: Text( RoomMusicTexts.t(context, "roomMusicDeleteConfirm", "确定删除音乐?"), style: const TextStyle(color: Colors.white), ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: Text(RoomMusicTexts.t(context, "cancel", "取消")), ), TextButton( onPressed: () => Navigator.pop(dialogContext, true), child: Text(RoomMusicTexts.t(context, "confirm", "确认")), ), ], ); }, ); if (result == true && mounted) { final deletedText = RoomMusicTexts.t( context, "roomMusicDeleted", "音乐已删除", ); final manager = context.read(); await manager.deleteMusic(item); SCTts.show(deletedText); } return false; } List _filteredSongs(List songs) { final keyword = _searchController.text.trim().toLowerCase(); if (keyword.isEmpty) { return songs; } return songs.where((item) { return item.title.toLowerCase().contains(keyword) || item.fileName.toLowerCase().contains(keyword); }).toList(); } } class _SongTile extends StatelessWidget { const _SongTile({ super.key, required this.item, required this.selected, required this.playing, required this.onTap, this.trailing, }); final SCMusicMode item; final bool selected; final bool playing; final VoidCallback onTap; final Widget? trailing; @override Widget build(BuildContext context) { return InkWell( onTap: onTap, child: Container( height: 62.w, decoration: BoxDecoration( color: selected ? Colors.white.withValues(alpha: 0.08) : Colors.transparent, border: Border( bottom: BorderSide(color: Colors.white.withValues(alpha: 0.08)), ), ), child: Row( children: [ Image.asset( playing ? "sc_images/room/sc_music_material_pause.png" : "sc_images/room/sc_music_material_play.png", width: 28.w, height: 28.w, ), SizedBox(width: 12.w), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: selected ? const Color(0xFF33E6A2) : Colors.white, fontSize: 14.sp, fontWeight: FontWeight.w600, ), ), SizedBox(height: 5.w), Text( _formatDuration(item.durationMs), style: TextStyle( color: Colors.white.withValues(alpha: 0.52), fontSize: 11.sp, ), ), ], ), ), if (trailing != null) trailing!, ], ), ), ); } String _formatDuration(int milliseconds) { final seconds = (milliseconds / 1000).floor(); final minutes = seconds ~/ 60; final rest = seconds % 60; return "$minutes:${rest.toString().padLeft(2, '0')}"; } }