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 Theme( data: Theme.of(context).copyWith( splashFactory: NoSplash.splashFactory, splashColor: Colors.transparent, highlightColor: Colors.transparent, ), child: Scaffold( backgroundColor: const Color(0xFF071F1B), body: SafeArea( child: Stack( children: [ Column( children: [ _buildHeader(context), Expanded( child: Consumer( builder: (context, manager, child) { final songs = _filteredSongs(manager.playlist); if (manager.playlist.isEmpty) { return _buildEmptyState(context); } return Column( children: [ _buildSearchField(), _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: [ _PlainIconButton( onPressed: () => Navigator.pop(context), child: Icon( Icons.arrow_back_ios_new, color: Colors.white, size: 24.w, ), ), Expanded( child: Text( RoomMusicTexts.t(context, "music", "音乐"), textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 18.sp, fontWeight: FontWeight.w800, ), ), ), _PlainIconButton( onPressed: _startAddFlow, child: Image.asset( "sc_images/room/sc_icon_room_music_add.png", width: 24.w, height: 24.w, ), ), ], ), ); } Widget _buildSearchField() { return Padding( padding: EdgeInsets.fromLTRB(16.w, 4.w, 16.w, 14.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(17.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: Row( children: [ Expanded( child: Text( template.replaceAll("{1}", count.toString()), style: TextStyle( color: Colors.white.withValues(alpha: 0.90), fontSize: 14.sp, ), ), ), _PlainIconButton( onPressed: _searchController.text.trim().isEmpty ? () { setState(() { _isEditing = !_isEditing; }); } : null, child: Image.asset( "sc_images/room/sc_music_material_edit.png", width: 24.w, height: 24.w, color: _isEditing ? const Color(0xFF33E6A2) : Colors.white.withValues( alpha: _searchController.text.trim().isEmpty ? 1 : 0.36, ), ), ), ], ), ); } 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), GestureDetector( behavior: HitTestBehavior.opaque, onTap: _startAddFlow, child: Container( width: 164.w, height: 42.w, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFF33E6A2), borderRadius: BorderRadius.circular(22.w), ), child: Text( RoomMusicTexts.t( context, "roomMusicScanFromPhone", "从手机扫描添加", ), style: TextStyle( color: const Color(0xFF06251F), 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 Padding( padding: EdgeInsets.only(bottom: 8.w), child: _SwipeDeleteTile( key: ValueKey("music-${item.id}"), onDelete: () { _confirmDelete(item); }, child: _SongTile( item: item, selected: manager.current?.id == item.id, playing: manager.current?.id == item.id && manager.isPlaying, onTap: () => manager.play(item), ), ), ); }, ); } 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), buildDefaultDragHandles: false, itemCount: songs.length, proxyDecorator: (child, index, animation) { return Material( color: Colors.transparent, shadowColor: Colors.transparent, surfaceTintColor: Colors.transparent, child: child, ); }, onReorder: (oldIndex, newIndex) { manager.reorder(oldIndex, newIndex); }, itemBuilder: (context, index) { final item = songs[index]; return Padding( key: ValueKey("reorder-${item.id}"), padding: EdgeInsets.only(bottom: 8.w), child: _SongTile( 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(_musicRoute(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({ 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 GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Container( decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFFB2FBCC), Color(0x00B2FBCC)], stops: [0, 1], ), borderRadius: BorderRadius.circular(8.w), ), child: Container( height: 54.w, margin: const EdgeInsets.all(1), padding: EdgeInsetsDirectional.fromSTEB(12.w, 0, 12.w, 0), decoration: BoxDecoration( color: const Color(0xFF08251E), borderRadius: BorderRadius.circular(7.w), ), child: Row( children: [ 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) ...[ SizedBox(width: 10.w), trailing!, ] else Image.asset( playing ? "sc_images/room/sc_music_material_pause.png" : "sc_images/room/sc_music_material_play.png", width: 26.w, height: 26.w, ), ], ), ), ), ); } String _formatDuration(int milliseconds) { final seconds = (milliseconds / 1000).floor(); final minutes = seconds ~/ 60; final rest = seconds % 60; return "$minutes:${rest.toString().padLeft(2, '0')}"; } } class _SwipeDeleteTile extends StatefulWidget { const _SwipeDeleteTile({ super.key, required this.child, required this.onDelete, }); final Widget child; final VoidCallback onDelete; @override State<_SwipeDeleteTile> createState() => _SwipeDeleteTileState(); } class _SwipeDeleteTileState extends State<_SwipeDeleteTile> { double _dragOffset = 0; @override Widget build(BuildContext context) { final actionWidth = 60.w; return LayoutBuilder( builder: (context, constraints) { return ClipRect( child: SizedBox( height: 56.w, child: GestureDetector( behavior: HitTestBehavior.opaque, onHorizontalDragUpdate: (details) { setState(() { _dragOffset = (_dragOffset + details.delta.dx).clamp( -actionWidth, 0, ); }); }, onHorizontalDragEnd: (_) { setState(() { _dragOffset = _dragOffset.abs() > actionWidth * 0.38 ? -actionWidth : 0; }); }, child: AnimatedContainer( duration: const Duration(milliseconds: 160), curve: Curves.easeOutCubic, transform: Matrix4.translationValues(_dragOffset, 0, 0), child: SizedBox( width: constraints.maxWidth + actionWidth, child: Row( children: [ SizedBox( width: constraints.maxWidth, child: widget.child, ), SizedBox( width: actionWidth, height: 56.w, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: widget.onDelete, child: Container( margin: EdgeInsetsDirectional.only(start: 4.w), decoration: BoxDecoration( color: const Color(0xFFE64A4A), borderRadius: BorderRadius.circular(8.w), ), alignment: Alignment.center, child: Image.asset( "sc_images/room/sc_music_material_delete.png", width: 22.w, height: 22.w, ), ), ), ), ], ), ), ), ), ), ); }, ); } } Route _musicRoute(Widget page) { return PageRouteBuilder( opaque: true, barrierColor: const Color(0xFF071F1B), transitionDuration: const Duration(milliseconds: 180), reverseTransitionDuration: const Duration(milliseconds: 160), pageBuilder: (context, animation, secondaryAnimation) => ColoredBox(color: const Color(0xFF071F1B), child: page), transitionsBuilder: (context, animation, secondaryAnimation, child) { final tween = Tween( begin: const Offset(1, 0), end: Offset.zero, ).chain(CurveTween(curve: Curves.easeOutCubic)); return SlideTransition(position: animation.drive(tween), child: child); }, ); } class _PlainIconButton extends StatelessWidget { const _PlainIconButton({required this.child, required this.onPressed}); final Widget child; final VoidCallback? onPressed; @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onPressed, child: SizedBox(width: 48.w, height: 48.w, child: Center(child: child)), ); } }