yumi-flutter/lib/modules/room/music/room_music_page.dart
2026-04-29 14:52:09 +08:00

471 lines
14 KiB
Dart

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<RoomMusicPage> createState() => _RoomMusicPageState();
}
class _RoomMusicPageState extends State<RoomMusicPage> {
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<RoomMusicManager>();
manager.attachRtcProvider(context.read<RtcProvider>());
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<RoomMusicManager>(
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<SCMusicMode> 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<SCMusicMode> 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<void> _startAddFlow() async {
if (_isScanning) {
return;
}
setState(() {
_isScanning = true;
});
final manager = context.read<RoomMusicManager>();
final folders = await _scanner.scanMp3Folders(addedSongs: manager.playlist);
if (!mounted) {
return;
}
setState(() {
_isScanning = false;
});
final songCount = folders.fold<int>(
0,
(sum, item) => sum + item.musicCount,
);
if (songCount == 0) {
SCTts.show(RoomMusicTexts.t(context, "roomMusicNotFound", "未找到音乐"));
return;
}
final changed = await Navigator.of(context).push<bool>(
MaterialPageRoute(builder: (_) => RoomMusicFolderPage(folders: folders)),
);
if (changed == true && mounted) {
await manager.reload();
}
}
Future<bool> _confirmDelete(SCMusicMode item) async {
final result = await showDialog<bool>(
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<RoomMusicManager>();
await manager.deleteMusic(item);
SCTts.show(deletedText);
}
return false;
}
List<SCMusicMode> _filteredSongs(List<SCMusicMode> 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')}";
}
}