bgm播放器 第二版
This commit is contained in:
parent
ddb378f5b4
commit
b0d68d2756
@ -9,24 +9,6 @@ import 'package:yumi/app/routes/sc_lk_application.dart';
|
||||
/// 安卓页面切换时长
|
||||
const Duration kAndroidTransitionDuration = Duration(milliseconds: 375);
|
||||
|
||||
/// 进入房间页动画时长
|
||||
const Duration kOpenRoomTransitionDuration = Duration(milliseconds: 325);
|
||||
|
||||
|
||||
/// 进入房间页过渡动画
|
||||
SlideTransition kOpenRoomTransitionBuilder( BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, 1.0),
|
||||
end: const Offset(0.0, 0.0),
|
||||
).animate(CurvedAnimation(parent: animation, curve: Curves.ease)),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
/// fluro的路由跳转工具类
|
||||
class SCNavigatorUtils {
|
||||
static bool inChatPage = false;
|
||||
|
||||
@ -61,6 +61,14 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
context,
|
||||
listen: false,
|
||||
).initializeRealTimeCommunicationManager(context);
|
||||
unawaited(
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).prewarmRtcEngine().catchError((error, stackTrace) {
|
||||
debugPrint('[Agora] prewarm on index failed: $error');
|
||||
}),
|
||||
);
|
||||
Provider.of<RtmProvider>(context, listen: false).init(context);
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
|
||||
@ -11,105 +11,115 @@ class RoomMusicFolderPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF071F1B),
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFF071F1B),
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
RoomMusicTexts.t(context, "roomMusicAddMusic", "添加音乐"),
|
||||
style: TextStyle(fontSize: 17.sp, fontWeight: FontWeight.w700),
|
||||
),
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
),
|
||||
body: ListView.separated(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 8.w, 16.w, 20.w),
|
||||
itemCount: folders.length,
|
||||
separatorBuilder:
|
||||
(_, __) =>
|
||||
Divider(color: Colors.white.withValues(alpha: 0.08), height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final folder = folders[index];
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
final changed = await Navigator.of(context).push<bool>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RoomMusicSelectPage(folder: folder),
|
||||
),
|
||||
);
|
||||
if (changed == true && context.mounted) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 82.w,
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
"sc_images/room/sc_music_material_folder.png",
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
folder.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (folder.addedCount > 0)
|
||||
Text(
|
||||
RoomMusicTexts.t(
|
||||
context,
|
||||
"roomMusicAddedCount",
|
||||
"已添加 ${folder.addedCount} 首",
|
||||
).replaceAll(
|
||||
"{1}",
|
||||
folder.addedCount.toString(),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF33E6A2),
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
Text(
|
||||
"${folder.musicCount} Music | ${folder.path}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textDirection: TextDirection.ltr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.52),
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFF071F1B),
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFF071F1B),
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
RoomMusicTexts.t(context, "roomMusicAddMusic", "添加音乐"),
|
||||
style: TextStyle(fontSize: 17.sp, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
body: ListView.separated(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 8.w, 16.w, 20.w),
|
||||
itemCount: folders.length,
|
||||
separatorBuilder:
|
||||
(_, __) => Divider(
|
||||
color: Colors.white.withValues(alpha: 0.08),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final folder = folders[index];
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
final changed = await Navigator.of(context).push<bool>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RoomMusicSelectPage(folder: folder),
|
||||
),
|
||||
);
|
||||
if (changed == true && context.mounted) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 82.w,
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
"sc_images/room/sc_music_material_folder.png",
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
folder.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (folder.addedCount > 0)
|
||||
Text(
|
||||
RoomMusicTexts.t(
|
||||
context,
|
||||
"roomMusicAddedCount",
|
||||
"已添加 ${folder.addedCount} 首",
|
||||
).replaceAll(
|
||||
"{1}",
|
||||
folder.addedCount.toString(),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF33E6A2),
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
Text(
|
||||
"${folder.musicCount} Music | ${folder.path}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textDirection: TextDirection.ltr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.52),
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -44,54 +44,63 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
||||
|
||||
@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),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
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<RoomMusicManager>(
|
||||
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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -102,9 +111,13 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
||||
height: 54.w,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
_PlainIconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
||||
child: Icon(
|
||||
Icons.arrow_back_ios_new,
|
||||
color: Colors.white,
|
||||
size: 24.w,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@ -117,26 +130,14 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
_PlainIconButton(
|
||||
onPressed: _startAddFlow,
|
||||
icon: Image.asset(
|
||||
child: 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -144,7 +145,7 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
||||
|
||||
Widget _buildSearchField() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 4.w, 16.w, 10.w),
|
||||
padding: EdgeInsets.fromLTRB(16.w, 4.w, 16.w, 14.w),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
@ -164,7 +165,7 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
||||
filled: true,
|
||||
fillColor: Colors.white.withValues(alpha: 0.08),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18.w),
|
||||
borderRadius: BorderRadius.circular(17.w),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
@ -180,15 +181,39 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
||||
);
|
||||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -215,19 +240,29 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
||||
),
|
||||
),
|
||||
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(
|
||||
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(fontSize: 14.sp, fontWeight: FontWeight.w700),
|
||||
child: Text(
|
||||
RoomMusicTexts.t(
|
||||
context,
|
||||
"roomMusicScanFromPhone",
|
||||
"从手机扫描添加",
|
||||
),
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF06251F),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -243,34 +278,20 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
||||
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,
|
||||
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),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -289,18 +310,21 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final item = songs[index];
|
||||
return _SongTile(
|
||||
return Padding(
|
||||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -389,7 +413,6 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
||||
|
||||
class _SongTile extends StatelessWidget {
|
||||
const _SongTile({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.selected,
|
||||
required this.playing,
|
||||
@ -405,57 +428,69 @@ class _SongTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
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)),
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFB2FBCC), Color(0x00B2FBCC)],
|
||||
stops: [0, 1],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
),
|
||||
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,
|
||||
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,
|
||||
SizedBox(height: 5.w),
|
||||
Text(
|
||||
_formatDuration(item.durationMs),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.52),
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -468,3 +503,94 @@ class _SongTile extends StatelessWidget {
|
||||
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 SizedBox(
|
||||
height: 56.w,
|
||||
child: Stack(
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
end: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: actionWidth,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: widget.onDelete,
|
||||
child: Container(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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: widget.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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,34 +42,41 @@ class _RoomMusicSelectPageState extends State<RoomMusicSelectPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF071F1B),
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFF071F1B),
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
RoomMusicTexts.t(context, "roomMusicAddMusic", "添加音乐"),
|
||||
style: TextStyle(fontSize: 17.sp, fontWeight: FontWeight.w700),
|
||||
),
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 8.w, 16.w, 16.w),
|
||||
itemCount: widget.folder.songs.length,
|
||||
separatorBuilder:
|
||||
(_, __) => Divider(
|
||||
color: Colors.white.withValues(alpha: 0.08),
|
||||
height: 1,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return _buildSongItem(widget.folder.songs[index]);
|
||||
},
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFF071F1B),
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFF071F1B),
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
RoomMusicTexts.t(context, "roomMusicAddMusic", "添加音乐"),
|
||||
style: TextStyle(fontSize: 17.sp, fontWeight: FontWeight.w700),
|
||||
),
|
||||
_buildBottomBar(context),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 8.w, 16.w, 16.w),
|
||||
itemCount: widget.folder.songs.length,
|
||||
separatorBuilder:
|
||||
(_, __) => Divider(
|
||||
color: Colors.white.withValues(alpha: 0.08),
|
||||
height: 1,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return _buildSongItem(widget.folder.songs[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
_buildBottomBar(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -84,7 +91,8 @@ class _RoomMusicSelectPageState extends State<RoomMusicSelectPage> {
|
||||
? Colors.white.withValues(alpha: 0.34)
|
||||
: const Color(0xFF33E6A2);
|
||||
|
||||
return InkWell(
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
@ -161,7 +169,8 @@ class _RoomMusicSelectPageState extends State<RoomMusicSelectPage> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
InkWell(
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (_allSelected) {
|
||||
@ -193,21 +202,31 @@ class _RoomMusicSelectPageState extends State<RoomMusicSelectPage> {
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
onPressed: _hasChanged ? _submit : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF33E6A2),
|
||||
disabledBackgroundColor: Colors.white.withValues(alpha: 0.18),
|
||||
foregroundColor: const Color(0xFF06251F),
|
||||
disabledForegroundColor: Colors.white.withValues(alpha: 0.38),
|
||||
shape: RoundedRectangleBorder(
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: _hasChanged ? _submit : null,
|
||||
child: Container(
|
||||
width: 104.w,
|
||||
height: 40.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
_hasChanged
|
||||
? const Color(0xFF33E6A2)
|
||||
: Colors.white.withValues(alpha: 0.18),
|
||||
borderRadius: BorderRadius.circular(20.w),
|
||||
),
|
||||
minimumSize: Size(104.w, 40.w),
|
||||
),
|
||||
child: Text(
|
||||
RoomMusicTexts.t(context, "add", "添加"),
|
||||
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w700),
|
||||
child: Text(
|
||||
RoomMusicTexts.t(context, "add", "添加"),
|
||||
style: TextStyle(
|
||||
color:
|
||||
_hasChanged
|
||||
? const Color(0xFF06251F)
|
||||
: Colors.white.withValues(alpha: 0.38),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -11,6 +11,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_screen.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/music/room_music_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/models/enum/sc_room_special_mike_type.dart';
|
||||
|
||||
///麦位
|
||||
@ -109,6 +110,22 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
width: widget.isGameModel ? 38.w : 52.w,
|
||||
height: widget.isGameModel ? 38.w : 52.w,
|
||||
)),
|
||||
Positioned(
|
||||
bottom: widget.isGameModel ? 2.w : 5.w,
|
||||
left: widget.isGameModel ? 2.w : 5.w,
|
||||
child:
|
||||
seatSnapshot.isCurrentUser
|
||||
? Selector<RoomMusicManager, bool>(
|
||||
selector:
|
||||
(_, manager) => manager.isPublishingToRoom,
|
||||
builder: (context, isPlayingBgm, child) {
|
||||
return isPlayingBgm
|
||||
? const _BgmPlayingIndicator()
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
Positioned(
|
||||
bottom: widget.isGameModel ? 2.w : 5.w,
|
||||
right: widget.isGameModel ? 2.w : 5.w,
|
||||
@ -254,6 +271,55 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
}
|
||||
}
|
||||
|
||||
class _BgmPlayingIndicator extends StatefulWidget {
|
||||
const _BgmPlayingIndicator();
|
||||
|
||||
@override
|
||||
State<_BgmPlayingIndicator> createState() => _BgmPlayingIndicatorState();
|
||||
}
|
||||
|
||||
class _BgmPlayingIndicatorState extends State<_BgmPlayingIndicator>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 900),
|
||||
)..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 12.w,
|
||||
height: 12.w,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF1E1A27),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: RotationTransition(
|
||||
turns: _controller,
|
||||
child: Image.asset(
|
||||
"sc_images/room/sc_music_user_bgm.png",
|
||||
width: 8.w,
|
||||
height: 8.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SeatRenderSnapshot {
|
||||
const _SeatRenderSnapshot({
|
||||
required this.isExitingCurrentVoiceRoomSession,
|
||||
@ -267,6 +333,7 @@ class _SeatRenderSnapshot {
|
||||
required this.userHeartbeatValue,
|
||||
required this.micLock,
|
||||
required this.micMute,
|
||||
required this.isCurrentUser,
|
||||
});
|
||||
|
||||
factory _SeatRenderSnapshot.fromProvider(RtcProvider provider, num index) {
|
||||
@ -287,6 +354,7 @@ class _SeatRenderSnapshot {
|
||||
userHeartbeatValue: user?.heartbeatVal ?? 0,
|
||||
micLock: roomSeat?.micLock ?? false,
|
||||
micMute: roomSeat?.micMute ?? false,
|
||||
isCurrentUser: provider.isOnMaiInIndex(index),
|
||||
);
|
||||
}
|
||||
|
||||
@ -301,6 +369,7 @@ class _SeatRenderSnapshot {
|
||||
final num userHeartbeatValue;
|
||||
final bool micLock;
|
||||
final bool micMute;
|
||||
final bool isCurrentUser;
|
||||
|
||||
bool get hasUser => userId.isNotEmpty;
|
||||
|
||||
@ -321,7 +390,8 @@ class _SeatRenderSnapshot {
|
||||
other.userVipName == userVipName &&
|
||||
other.userHeartbeatValue == userHeartbeatValue &&
|
||||
other.micLock == micLock &&
|
||||
other.micMute == micMute;
|
||||
other.micMute == micMute &&
|
||||
other.isCurrentUser == isCurrentUser;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -337,6 +407,7 @@ class _SeatRenderSnapshot {
|
||||
userHeartbeatValue,
|
||||
micLock,
|
||||
micMute,
|
||||
isCurrentUser,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:async';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@ -31,7 +30,7 @@ import 'package:yumi/ui_kit/widgets/room/room_head_widget.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_online_user_widget.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_play_widget.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/music/room_music_floating_entry.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/music/room_music_room_player.dart';
|
||||
import 'package:yumi/shared/data_sources/models/enum/sc_gift_type.dart';
|
||||
|
||||
import '../../ui_kit/components/sc_float_ichart.dart';
|
||||
@ -73,15 +72,6 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
_tabController = TabController(length: _pages.length, vsync: this);
|
||||
_enableRoomVisualEffects();
|
||||
_tabController.addListener(_handleTabChange);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final rtcProvider = context.read<RtcProvider>();
|
||||
rtcProvider.requestMicrophoneListRefresh(notifyIfUnchanged: true);
|
||||
rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: true);
|
||||
});
|
||||
|
||||
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
|
||||
event,
|
||||
) {
|
||||
@ -179,24 +169,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
if (snapshot.status != RoomStartupStatus.loading) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Positioned.fill(
|
||||
child: AbsorbPointer(
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0.18),
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const CupertinoActivityIndicator(color: Colors.white24),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -329,7 +302,12 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
],
|
||||
),
|
||||
_buildRoomStartupLayer(),
|
||||
const RoomMusicFloatingEntry(),
|
||||
const PositionedDirectional(
|
||||
start: 0,
|
||||
end: 0,
|
||||
bottom: 0,
|
||||
child: RoomMusicRoomPlayer(),
|
||||
),
|
||||
// _buildPlayViews(),
|
||||
///幸运礼物中奖动画
|
||||
LuckGiftNomorAnimWidget(),
|
||||
|
||||
@ -21,11 +21,27 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
static String roomBackgroundUpload = '/room/background/upload';
|
||||
static String roomMusic = '/room/music';
|
||||
|
||||
static Route<T> _buildRoute<T>(Widget page) {
|
||||
static Route<T> _buildRoute<T>(Widget page, {RouteSettings? settings}) {
|
||||
if (Platform.isIOS) {
|
||||
return CupertinoPageRoute<T>(builder: (_) => page);
|
||||
return CupertinoPageRoute<T>(builder: (_) => page, settings: settings);
|
||||
}
|
||||
return MaterialPageRoute<T>(builder: (_) => page);
|
||||
return MaterialPageRoute<T>(builder: (_) => page, settings: settings);
|
||||
}
|
||||
|
||||
static Future<T?> openVoiceRoom<T>(
|
||||
BuildContext context, {
|
||||
bool replace = false,
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
final navigator = Navigator.of(context, rootNavigator: rootNavigator);
|
||||
final route = _buildRoute<T>(
|
||||
const VoiceRoomPage(),
|
||||
settings: RouteSettings(name: voiceRoom),
|
||||
);
|
||||
if (replace) {
|
||||
return navigator.pushReplacement<T, T>(route);
|
||||
}
|
||||
return navigator.push<T>(route);
|
||||
}
|
||||
|
||||
static Future<T?> openRoomEdit<T>(
|
||||
|
||||
@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||
import 'package:fluro/fluro.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
@ -21,7 +20,6 @@ import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/routes/sc_routes.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
@ -53,9 +51,35 @@ enum RoomStartupStatus { idle, loading, ready, failed }
|
||||
|
||||
enum RoomStartupFailureType { none, entry, im, rtc }
|
||||
|
||||
class _RoomStartupAsyncResult<T> {
|
||||
const _RoomStartupAsyncResult.value(this.value)
|
||||
: error = null,
|
||||
stackTrace = null;
|
||||
|
||||
const _RoomStartupAsyncResult.error(this.error, this.stackTrace)
|
||||
: value = null;
|
||||
|
||||
final T? value;
|
||||
final Object? error;
|
||||
final StackTrace? stackTrace;
|
||||
|
||||
T requireValue() {
|
||||
final capturedError = error;
|
||||
if (capturedError != null) {
|
||||
Error.throwWithStackTrace(
|
||||
capturedError,
|
||||
stackTrace ?? StackTrace.current,
|
||||
);
|
||||
}
|
||||
return value as T;
|
||||
}
|
||||
}
|
||||
|
||||
class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
static const Duration _micListPollingInterval = Duration(seconds: 2);
|
||||
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
|
||||
static const int _exitNetworkRetryLimit = 3;
|
||||
static const Duration _exitNetworkRetryDelay = Duration(milliseconds: 500);
|
||||
static const Duration _selfMicStateGracePeriod = Duration(seconds: 4);
|
||||
static const Duration _giftTriggeredMicRefreshMinInterval = Duration(
|
||||
milliseconds: 900,
|
||||
@ -69,6 +93,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
bool _isHandlingAgoraRoomFailure = false;
|
||||
bool _agoraJoined = false;
|
||||
bool _agoraRoleIsBroadcaster = false;
|
||||
bool _agoraAudioConfigured = false;
|
||||
RoomStartupStatus _roomStartupStatus = RoomStartupStatus.idle;
|
||||
RoomStartupFailureType _roomStartupFailureType = RoomStartupFailureType.none;
|
||||
int _roomStartupFailureToken = 0;
|
||||
@ -78,6 +103,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
Timer? _joinAgoraTimeoutTimer;
|
||||
Timer? _agoraDisconnectedCleanupTimer;
|
||||
Completer<void>? _joinAgoraCompleter;
|
||||
Future<RtcEngine>? _rtcEngineInitTask;
|
||||
Future<void>? _rtcEnginePrewarmTask;
|
||||
String? _joiningChannelId;
|
||||
bool _isRefreshingMicList = false;
|
||||
bool _isRefreshingOnlineUsers = false;
|
||||
@ -948,6 +975,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_resyncHeartbeatFromAgoraState();
|
||||
}
|
||||
|
||||
Future<void> _leaveAgoraRoomForExit() async {
|
||||
final rtcEngine = engine;
|
||||
_resetAgoraTracking();
|
||||
if (rtcEngine == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await rtcEngine.leaveChannel();
|
||||
} catch (error) {
|
||||
debugPrint('[RoomExit] leave Agora channel failed: $error');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleAgoraJoinFailed(
|
||||
Object error, {
|
||||
required bool exitRoom,
|
||||
@ -985,7 +1025,55 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> joinAgoraVoiceChannel({bool throwOnError = false}) async {
|
||||
Future<void> prewarmRtcEngine() {
|
||||
final existingTask = _rtcEnginePrewarmTask;
|
||||
if (existingTask != null) {
|
||||
return existingTask;
|
||||
}
|
||||
final task = _prewarmRtcEngine();
|
||||
_rtcEnginePrewarmTask = task;
|
||||
return task;
|
||||
}
|
||||
|
||||
Future<void> _prewarmRtcEngine() async {
|
||||
try {
|
||||
final rtcEngine = await _initAgoraRtcEngine();
|
||||
await _configureAgoraAudioEngine(rtcEngine);
|
||||
} catch (error, stackTrace) {
|
||||
_rtcEnginePrewarmTask = null;
|
||||
debugPrint('[Agora] prewarm failed: $error\n$stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _fetchRtcTokenForCurrentRoom({
|
||||
required bool isPublisher,
|
||||
}) async {
|
||||
final channelId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||
final userId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
final rtcToken = await SCAccountRepository().getRtcToken(
|
||||
channelId,
|
||||
userId,
|
||||
isPublisher: isPublisher,
|
||||
);
|
||||
return rtcToken.rtcToken ?? "";
|
||||
}
|
||||
|
||||
Future<_RoomStartupAsyncResult<T>> _captureStartupFuture<T>(
|
||||
Future<T> future,
|
||||
) async {
|
||||
try {
|
||||
return _RoomStartupAsyncResult<T>.value(await future);
|
||||
} catch (error, stackTrace) {
|
||||
return _RoomStartupAsyncResult<T>.error(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> joinAgoraVoiceChannel({
|
||||
bool throwOnError = false,
|
||||
String? rtcToken,
|
||||
}) async {
|
||||
final channelId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||
if (channelId.isEmpty) {
|
||||
final error = StateError('Agora channel id is empty');
|
||||
@ -1013,23 +1101,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
try {
|
||||
final rtcEngine = await _initAgoraRtcEngine();
|
||||
rtcEngine.setAudioProfile(
|
||||
profile: AudioProfileType.audioProfileSpeechStandard,
|
||||
scenario: AudioScenarioType.audioScenarioGameStreaming,
|
||||
);
|
||||
rtcEngine.enableAudioVolumeIndication(
|
||||
interval: 500,
|
||||
smooth: 3,
|
||||
reportVad: true,
|
||||
);
|
||||
await rtcEngine.disableVideo();
|
||||
var rtcToken = await SCAccountRepository().getRtcToken(
|
||||
channelId,
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
|
||||
isPublisher: false,
|
||||
);
|
||||
await _configureAgoraAudioEngine(rtcEngine);
|
||||
final resolvedRtcToken =
|
||||
rtcToken ?? await _fetchRtcTokenForCurrentRoom(isPublisher: false);
|
||||
await rtcEngine.joinChannel(
|
||||
token: rtcToken.rtcToken ?? "",
|
||||
token: resolvedRtcToken,
|
||||
channelId: channelId,
|
||||
uid: _resolveAgoraUidForCurrentUser(),
|
||||
options: const ChannelMediaOptions(
|
||||
@ -1054,7 +1130,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (throwOnError) {
|
||||
rethrow;
|
||||
}
|
||||
print('加入失败:${e.runtimeType},${e.toString()}');
|
||||
debugPrint('加入失败:${e.runtimeType},${e.toString()}');
|
||||
} finally {
|
||||
if (identical(_joinAgoraCompleter, joinCompleter)) {
|
||||
_cancelAgoraJoinWaiter();
|
||||
@ -1165,10 +1241,32 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<RtcEngine> _initAgoraRtcEngine() async {
|
||||
Future<RtcEngine> _initAgoraRtcEngine() {
|
||||
if (engine != null) {
|
||||
return engine!;
|
||||
return Future.value(engine!);
|
||||
}
|
||||
final existingTask = _rtcEngineInitTask;
|
||||
if (existingTask != null) {
|
||||
return existingTask;
|
||||
}
|
||||
final task = _createAgoraRtcEngine();
|
||||
_rtcEngineInitTask = task;
|
||||
void clearInitTask() {
|
||||
if (identical(_rtcEngineInitTask, task)) {
|
||||
_rtcEngineInitTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
unawaited(
|
||||
task.then<void>(
|
||||
(_) => clearInitTask(),
|
||||
onError: (_, __) => clearInitTask(),
|
||||
),
|
||||
);
|
||||
return task;
|
||||
}
|
||||
|
||||
Future<RtcEngine> _createAgoraRtcEngine() async {
|
||||
final rtcEngine = createAgoraRtcEngine();
|
||||
await rtcEngine.initialize(
|
||||
RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid),
|
||||
@ -1176,7 +1274,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
engine = rtcEngine;
|
||||
_rtcEngineEventHandler = RtcEngineEventHandler(
|
||||
onError: (ErrorCodeType err, String msg) {
|
||||
print('rtc错误$err');
|
||||
debugPrint('rtc错误$err');
|
||||
final error = 'Agora error: $err $msg';
|
||||
if (!_agoraJoined) {
|
||||
final completer = _joinAgoraCompleter;
|
||||
@ -1192,7 +1290,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
},
|
||||
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
|
||||
print('rtc 自己加入 ${connection.channelId} ${connection.localUid}');
|
||||
debugPrint('rtc 自己加入 ${connection.channelId} ${connection.localUid}');
|
||||
final joinedChannelId = connection.channelId ?? "";
|
||||
if (!_isCurrentAgoraChannel(joinedChannelId)) {
|
||||
return;
|
||||
@ -1309,11 +1407,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_roomMusicMixingStateListener?.call(state, reason);
|
||||
},
|
||||
onUserJoined: (connection, remoteUid, elapsed) {
|
||||
print('rtc用户 $remoteUid 加入了频道');
|
||||
debugPrint('rtc用户 $remoteUid 加入了频道');
|
||||
},
|
||||
// 监听远端用户离开
|
||||
onUserOffline: (connection, remoteUid, reason) {
|
||||
print('rtc用户 $remoteUid 离开了频道 (原因: $reason)');
|
||||
debugPrint('rtc用户 $remoteUid 离开了频道 (原因: $reason)');
|
||||
},
|
||||
onTokenPrivilegeWillExpire: (
|
||||
RtcConnection connection,
|
||||
@ -1329,9 +1427,27 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
onAudioVolumeIndication: initializeAudioVolumeIndicationCallback,
|
||||
);
|
||||
rtcEngine.registerEventHandler(_rtcEngineEventHandler!);
|
||||
await _configureAgoraAudioEngine(rtcEngine);
|
||||
return rtcEngine;
|
||||
}
|
||||
|
||||
Future<void> _configureAgoraAudioEngine(RtcEngine rtcEngine) async {
|
||||
if (_agoraAudioConfigured) {
|
||||
return;
|
||||
}
|
||||
await rtcEngine.setAudioProfile(
|
||||
profile: AudioProfileType.audioProfileSpeechStandard,
|
||||
scenario: AudioScenarioType.audioScenarioGameStreaming,
|
||||
);
|
||||
await rtcEngine.enableAudioVolumeIndication(
|
||||
interval: 500,
|
||||
smooth: 3,
|
||||
reportVad: true,
|
||||
);
|
||||
await rtcEngine.disableVideo();
|
||||
_agoraAudioConfigured = true;
|
||||
}
|
||||
|
||||
Future<void> releaseRtcEngineForAppTermination() async {
|
||||
final rtcEngine = engine;
|
||||
if (rtcEngine == null) {
|
||||
@ -1341,6 +1457,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
final eventHandler = _rtcEngineEventHandler;
|
||||
engine = null;
|
||||
_rtcEngineEventHandler = null;
|
||||
_rtcEngineInitTask = null;
|
||||
_rtcEnginePrewarmTask = null;
|
||||
_agoraAudioConfigured = false;
|
||||
|
||||
try {
|
||||
if (eventHandler != null) {
|
||||
@ -1348,14 +1467,14 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
await rtcEngine.leaveChannel();
|
||||
} catch (e) {
|
||||
print('rtc销毁前离开频道出错: $e');
|
||||
debugPrint('rtc销毁前离开频道出错: $e');
|
||||
}
|
||||
_resetAgoraTracking();
|
||||
|
||||
try {
|
||||
await rtcEngine.release();
|
||||
} catch (e) {
|
||||
print('rtc释放引擎出错: $e');
|
||||
debugPrint('rtc释放引擎出错: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1506,35 +1625,16 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_setRoomStartupReady();
|
||||
setRoomVisualEffectsEnabled(true);
|
||||
SCFloatIchart().remove();
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
'${VoiceRoomRoute.voiceRoom}?id=$roomId',
|
||||
transition: TransitionType.custom,
|
||||
transitionDuration: kOpenRoomTransitionDuration,
|
||||
transitionBuilder: (
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return kOpenRoomTransitionBuilder(
|
||||
context,
|
||||
animation,
|
||||
secondaryAnimation,
|
||||
child,
|
||||
);
|
||||
},
|
||||
);
|
||||
VoiceRoomRoute.openVoiceRoom(context);
|
||||
} else {
|
||||
SCFloatIchart().remove();
|
||||
if (currenRoom != null) {
|
||||
await exitCurrentVoiceRoomSession(false);
|
||||
await exitCurrentVoiceRoomSession(false, deferRemoteCleanup: false);
|
||||
}
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||
SCLoadingManager.show(context: context);
|
||||
try {
|
||||
currenRoom = await SCAccountRepository().entryRoom(
|
||||
roomId,
|
||||
@ -1550,8 +1650,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
} catch (e) {
|
||||
await resetLocalRoomState(fallbackRtmProvider: rtmProvider);
|
||||
rethrow;
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1586,15 +1684,22 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
_setRoomStartupLoading();
|
||||
setRoomVisualEffectsEnabled(true);
|
||||
SCNavigatorUtils.push(context!, VoiceRoomRoute.voiceRoom, replace: false);
|
||||
VoiceRoomRoute.openVoiceRoom(context!);
|
||||
unawaited(_bootstrapEnteredVoiceRoomSession());
|
||||
}
|
||||
|
||||
Future<void> _bootstrapEnteredVoiceRoomSession() async {
|
||||
var failureType = RoomStartupFailureType.im;
|
||||
try {
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
|
||||
final joinResult = await rtmProvider?.joinRoomGroup(groupId, "");
|
||||
final joinGroupFuture = rtmProvider?.joinRoomGroup(groupId, "");
|
||||
final rtcTokenFuture = _captureStartupFuture(
|
||||
_fetchRtcTokenForCurrentRoom(isPublisher: false),
|
||||
);
|
||||
final microphoneListFuture = retrieveMicrophoneList();
|
||||
final onlineUsersFuture = fetchOnlineUsersList();
|
||||
final joinResult = await joinGroupFuture;
|
||||
if (joinResult == null || joinResult.code != 0) {
|
||||
debugPrint(
|
||||
'[RoomStartup] join IM group failed code=${joinResult?.code} desc=${joinResult?.desc}',
|
||||
@ -1618,19 +1723,16 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
),
|
||||
);
|
||||
|
||||
///获取麦位
|
||||
retrieveMicrophoneList();
|
||||
fetchOnlineUsersList();
|
||||
await Future.wait([microphoneListFuture, onlineUsersFuture]);
|
||||
_startRoomStatePolling();
|
||||
Provider.of<SocialChatRoomManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).fetchContributionLevelData(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
);
|
||||
).fetchContributionLevelData(roomId);
|
||||
|
||||
failureType = RoomStartupFailureType.rtc;
|
||||
await joinAgoraVoiceChannel(throwOnError: true);
|
||||
final rtcToken = (await rtcTokenFuture).requireValue();
|
||||
await joinAgoraVoiceChannel(throwOnError: true, rtcToken: rtcToken);
|
||||
await _bootstrapRoomHeartbeatState();
|
||||
_setRoomStartupReady();
|
||||
|
||||
@ -1818,40 +1920,141 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future exitCurrentVoiceRoomSession(bool isLogout) async {
|
||||
Future exitCurrentVoiceRoomSession(
|
||||
bool isLogout, {
|
||||
bool deferRemoteCleanup = true,
|
||||
}) async {
|
||||
if (_isExitingCurrentVoiceRoomSession) {
|
||||
return;
|
||||
}
|
||||
_setExitingCurrentVoiceRoomSession(true);
|
||||
_stopRoomStatePolling();
|
||||
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
final exitRtmProvider = rtmProvider;
|
||||
final navigationContext = context;
|
||||
try {
|
||||
rtmProvider?.msgAllListener = null;
|
||||
rtmProvider?.msgChatListener = null;
|
||||
rtmProvider?.msgGiftListener = null;
|
||||
SCLoadingManager.show(context: context);
|
||||
exitRtmProvider?.msgAllListener = null;
|
||||
exitRtmProvider?.msgChatListener = null;
|
||||
exitRtmProvider?.msgGiftListener = null;
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
setRoomVisualEffectsEnabled(false);
|
||||
SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false);
|
||||
SCHeartbeatUtils.cancelAnchorTimer();
|
||||
rtmProvider?.cleanRoomData();
|
||||
await _cleanupAgoraRoomState();
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
await rtmProvider?.quitGroup(
|
||||
currenRoom!.roomProfile?.roomProfile?.roomAccount ?? "",
|
||||
);
|
||||
await SCAccountRepository().quitRoom(
|
||||
currenRoom!.roomProfile?.roomProfile?.id ?? "",
|
||||
);
|
||||
_clearData();
|
||||
SCLoadingManager.hide();
|
||||
if (!isLogout) {
|
||||
SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home));
|
||||
}
|
||||
SCHeartbeatUtils.cancelTimer();
|
||||
exitRtmProvider?.cleanRoomData();
|
||||
} catch (e) {
|
||||
print('rtc退出房间出错: $e');
|
||||
_clearData();
|
||||
SCLoadingManager.hide();
|
||||
if (!isLogout) {
|
||||
SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home));
|
||||
debugPrint('rtc退出房间本地清理出错: $e');
|
||||
}
|
||||
|
||||
_clearData();
|
||||
if (!isLogout && navigationContext != null) {
|
||||
SCNavigatorUtils.popUntil(
|
||||
navigationContext,
|
||||
ModalRoute.withName(SCRoutes.home),
|
||||
);
|
||||
}
|
||||
|
||||
final agoraLeaveTask = _leaveAgoraRoomForExit();
|
||||
final remoteCleanupTask = _cleanupExitedRoomRemotely(
|
||||
exitRtmProvider: exitRtmProvider,
|
||||
groupId: groupId,
|
||||
roomId: roomId,
|
||||
shouldSendOnlineHeartbeat: !isLogout,
|
||||
);
|
||||
if (deferRemoteCleanup) {
|
||||
unawaited(agoraLeaveTask);
|
||||
unawaited(remoteCleanupTask);
|
||||
} else {
|
||||
await agoraLeaveTask;
|
||||
await remoteCleanupTask;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cleanupExitedRoomRemotely({
|
||||
required RtmProvider? exitRtmProvider,
|
||||
required String groupId,
|
||||
required String roomId,
|
||||
required bool shouldSendOnlineHeartbeat,
|
||||
}) async {
|
||||
final tasks = <Future<void>>[];
|
||||
if (shouldSendOnlineHeartbeat) {
|
||||
tasks.add(
|
||||
_retryExitNetworkRequest('heartbeat ONLINE', () async {
|
||||
if (currenRoom != null) {
|
||||
debugPrint('[RoomExit] skip ONLINE heartbeat: already in a room');
|
||||
return;
|
||||
}
|
||||
await SCHeartbeatUtils.scheduleHeartbeat(
|
||||
SCHeartbeatStatus.ONLINE.name,
|
||||
false,
|
||||
rethrowOnError: true,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (groupId.isNotEmpty && exitRtmProvider != null) {
|
||||
tasks.add(
|
||||
_retryExitNetworkRequest('quit IM group $groupId', () async {
|
||||
if (_isCurrentRoomGroup(groupId)) {
|
||||
debugPrint('[RoomExit] skip quitGroup: re-entered group $groupId');
|
||||
return;
|
||||
}
|
||||
await exitRtmProvider.quitGroup(groupId);
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (roomId.isNotEmpty) {
|
||||
tasks.add(
|
||||
_retryExitNetworkRequest('quit room $roomId', () async {
|
||||
if (_isCurrentRoomId(roomId)) {
|
||||
debugPrint('[RoomExit] skip quitRoom: re-entered room $roomId');
|
||||
return;
|
||||
}
|
||||
final didQuit = await SCAccountRepository().quitRoom(roomId);
|
||||
if (!didQuit) {
|
||||
throw StateError('quitRoom returned false');
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
await Future.wait(tasks);
|
||||
}
|
||||
|
||||
Future<void> _retryExitNetworkRequest(
|
||||
String name,
|
||||
Future<void> Function() action,
|
||||
) async {
|
||||
Object? lastError;
|
||||
StackTrace? lastStackTrace;
|
||||
for (var attempt = 1; attempt <= _exitNetworkRetryLimit; attempt++) {
|
||||
try {
|
||||
await action();
|
||||
return;
|
||||
} catch (error, stackTrace) {
|
||||
lastError = error;
|
||||
lastStackTrace = stackTrace;
|
||||
debugPrint(
|
||||
'[RoomExit] $name failed attempt '
|
||||
'$attempt/$_exitNetworkRetryLimit: $error',
|
||||
);
|
||||
if (attempt < _exitNetworkRetryLimit) {
|
||||
await Future.delayed(_exitNetworkRetryDelay * attempt);
|
||||
}
|
||||
}
|
||||
}
|
||||
debugPrint(
|
||||
'[RoomExit] $name failed after $_exitNetworkRetryLimit attempts, '
|
||||
'treat as backend/remote issue: $lastError\n$lastStackTrace',
|
||||
);
|
||||
}
|
||||
|
||||
bool _isCurrentRoomId(String roomId) {
|
||||
return roomId.isNotEmpty &&
|
||||
currenRoom?.roomProfile?.roomProfile?.id == roomId;
|
||||
}
|
||||
|
||||
bool _isCurrentRoomGroup(String groupId) {
|
||||
return groupId.isNotEmpty &&
|
||||
currenRoom?.roomProfile?.roomProfile?.roomAccount == groupId;
|
||||
}
|
||||
|
||||
Future<void> resetLocalRoomState({RtmProvider? fallbackRtmProvider}) async {
|
||||
@ -1867,7 +2070,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
await _cleanupAgoraRoomState();
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
} catch (e) {
|
||||
print('rtc清理本地房间状态出错: $e');
|
||||
debugPrint('rtc清理本地房间状态出错: $e');
|
||||
} finally {
|
||||
rtmProvider?.cleanRoomData();
|
||||
_clearData();
|
||||
@ -1903,7 +2106,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
await SCAccountRepository().quitRoom(roomId);
|
||||
}
|
||||
} catch (e) {
|
||||
print('rtc进房失败清理本地房间状态出错: $e');
|
||||
debugPrint('rtc进房失败清理本地房间状态出错: $e');
|
||||
} finally {
|
||||
_clearData();
|
||||
_isHandlingRoomStartupFailure = false;
|
||||
@ -2098,12 +2301,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
addSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) {
|
||||
_onSoundVoiceChangeList.add(onSoundVoiceChange);
|
||||
print('添加监听:${_onSoundVoiceChangeList.length}');
|
||||
debugPrint('添加监听:${_onSoundVoiceChangeList.length}');
|
||||
}
|
||||
|
||||
removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) {
|
||||
_onSoundVoiceChangeList.remove(onSoundVoiceChange);
|
||||
print('删除监听:${_onSoundVoiceChangeList.length}');
|
||||
debugPrint('删除监听:${_onSoundVoiceChangeList.length}');
|
||||
}
|
||||
|
||||
void shangMai(num index, {String? eventType, String? inviterId}) async {
|
||||
@ -2286,7 +2489,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
});
|
||||
} catch (e) {
|
||||
SCTts.show("kill the user fail");
|
||||
print('踢人下麦失败');
|
||||
debugPrint('踢人下麦失败');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
bool _isStarting = false;
|
||||
bool _isStoppingByUser = false;
|
||||
bool _lastLoopback = true;
|
||||
bool _roomPlayerVisible = false;
|
||||
|
||||
List<SCMusicMode> get playlist => List.unmodifiable(_playlist);
|
||||
|
||||
@ -56,6 +57,10 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
|
||||
bool get hasCurrent => _current != null;
|
||||
|
||||
bool get roomPlayerVisible => _roomPlayerVisible;
|
||||
|
||||
bool get isPublishingToRoom => _isPlaying && !_lastLoopback;
|
||||
|
||||
int get currentIndex {
|
||||
final currentId = _current?.id;
|
||||
if (currentId == null) {
|
||||
@ -144,6 +149,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
_durationMs = item.durationMs;
|
||||
_positionMs = startPositionMs;
|
||||
_isPaused = false;
|
||||
_roomPlayerVisible = true;
|
||||
await _startCurrent(startPositionMs: startPositionMs);
|
||||
}
|
||||
|
||||
@ -188,6 +194,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
if (clearCurrent) {
|
||||
_current = null;
|
||||
_durationMs = 0;
|
||||
_roomPlayerVisible = false;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
@ -261,6 +268,22 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void toggleRoomPlayerVisible() {
|
||||
if (_current == null) {
|
||||
return;
|
||||
}
|
||||
_roomPlayerVisible = !_roomPlayerVisible;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void hideRoomPlayer() {
|
||||
if (!_roomPlayerVisible) {
|
||||
return;
|
||||
}
|
||||
_roomPlayerVisible = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> syncPlaybackRouting() async {
|
||||
final current = _current;
|
||||
final rtcProvider = _rtcProvider;
|
||||
@ -430,6 +453,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
_current = null;
|
||||
_isPlaying = false;
|
||||
_isPaused = false;
|
||||
_roomPlayerVisible = false;
|
||||
_positionMs = 0;
|
||||
_durationMs = 0;
|
||||
_stopPositionTimer();
|
||||
|
||||
@ -17,6 +17,7 @@ class SCHeartbeatUtils {
|
||||
String status,
|
||||
bool upMick, {
|
||||
String? roomId,
|
||||
bool rethrowOnError = false,
|
||||
}) async {
|
||||
cancelTimer();
|
||||
_c = status;
|
||||
@ -40,6 +41,9 @@ class SCHeartbeatUtils {
|
||||
try {
|
||||
cancelTimer();
|
||||
} catch (_) {}
|
||||
if (rethrowOnError) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -109,10 +109,7 @@ class SCChatRoomHelper {
|
||||
).retrieveMicrophoneList();
|
||||
Provider.of<RealTimeCommunicationManager>(context, listen: false)
|
||||
.closeFullGame = true;
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
'${VoiceRoomRoute.voiceRoom}?id=${Provider.of<RealTimeCommunicationManager>(context, listen: false).currenRoom?.roomProfile?.roomProfile?.id}',
|
||||
);
|
||||
VoiceRoomRoute.openVoiceRoom(context);
|
||||
}
|
||||
|
||||
static double getCurrenProgress(
|
||||
|
||||
@ -1,133 +1,96 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/routes/sc_routes.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart';
|
||||
import 'package:yumi/services/gift/gift_animation_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
|
||||
import '../../components/sc_float_ichart.dart';
|
||||
|
||||
class ExitMinRoomPage extends StatefulWidget {
|
||||
bool isFz;
|
||||
String roomId;
|
||||
|
||||
ExitMinRoomPage(this.isFz, this.roomId);
|
||||
|
||||
@override
|
||||
_ExitMinRoomPageState createState() => _ExitMinRoomPageState();
|
||||
}
|
||||
|
||||
class _ExitMinRoomPageState extends State<ExitMinRoomPage> {
|
||||
SCUserIdentityRes? userIdentity;
|
||||
bool isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SCAccountRepository()
|
||||
.userIdentity(
|
||||
userId:
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).currenRoom?.roomProfile?.roomProfile?.userId,
|
||||
)
|
||||
.then((v) {
|
||||
userIdentity = v;
|
||||
isLoading = false;
|
||||
setState(() {});
|
||||
})
|
||||
.catchError((e) {
|
||||
isLoading = false;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SCDebounceWidget(
|
||||
child: Container(color: Colors.transparent),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
Column(
|
||||
spacing: 55.w,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SCDebounceWidget(
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
"sc_images/room/sc_icon_min_room.png",
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.mInimize,
|
||||
fontSize: 15.sp,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
SCFloatIchart().show();
|
||||
SCNavigatorUtils.popUntil(
|
||||
context,
|
||||
ModalRoute.withName(SCRoutes.home),
|
||||
);
|
||||
Provider.of<GiftAnimationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).cleanupAnimationResources();
|
||||
},
|
||||
),
|
||||
SCDebounceWidget(
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
"sc_images/room/sc_icon_exit_room.png",
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.exit,
|
||||
fontSize: 15.sp,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
SCNavigatorUtils.goBack(context);
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).exitCurrentVoiceRoomSession(false);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/routes/sc_routes.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/services/gift/gift_animation_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
|
||||
import '../../components/sc_float_ichart.dart';
|
||||
|
||||
class ExitMinRoomPage extends StatefulWidget {
|
||||
final bool isFz;
|
||||
final String roomId;
|
||||
|
||||
const ExitMinRoomPage(this.isFz, this.roomId, {super.key});
|
||||
|
||||
@override
|
||||
State<ExitMinRoomPage> createState() => _ExitMinRoomPageState();
|
||||
}
|
||||
|
||||
class _ExitMinRoomPageState extends State<ExitMinRoomPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SCDebounceWidget(
|
||||
child: Container(color: Colors.transparent),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
Column(
|
||||
spacing: 55.w,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SCDebounceWidget(
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
"sc_images/room/sc_icon_min_room.png",
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.mInimize,
|
||||
fontSize: 15.sp,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final giftAnimationManager =
|
||||
context.read<GiftAnimationManager>();
|
||||
SCFloatIchart().show();
|
||||
SCNavigatorUtils.popUntil(
|
||||
context,
|
||||
ModalRoute.withName(SCRoutes.home),
|
||||
);
|
||||
giftAnimationManager.cleanupAnimationResources();
|
||||
},
|
||||
),
|
||||
SCDebounceWidget(
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
"sc_images/room/sc_icon_exit_room.png",
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.exit,
|
||||
fontSize: 15.sp,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final rtcProvider = context.read<RtcProvider>();
|
||||
SCNavigatorUtils.goBack(context);
|
||||
rtcProvider.exitCurrentVoiceRoomSession(false);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,66 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/modules/room/voice_room_route.dart';
|
||||
import 'package:yumi/services/music/room_music_manager.dart';
|
||||
|
||||
class RoomMusicFloatingEntry extends StatefulWidget {
|
||||
class RoomMusicFloatingEntry extends StatelessWidget {
|
||||
const RoomMusicFloatingEntry({super.key});
|
||||
|
||||
@override
|
||||
State<RoomMusicFloatingEntry> createState() => _RoomMusicFloatingEntryState();
|
||||
}
|
||||
|
||||
class _RoomMusicFloatingEntryState extends State<RoomMusicFloatingEntry>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<RoomMusicManager, _FloatingMusicSnapshot>(
|
||||
selector:
|
||||
(_, manager) => _FloatingMusicSnapshot(
|
||||
visible: manager.current != null,
|
||||
playing: manager.isPlaying,
|
||||
),
|
||||
builder: (context, snapshot, child) {
|
||||
if (!snapshot.visible) {
|
||||
_controller.stop();
|
||||
return Selector<RoomMusicManager, bool>(
|
||||
selector: (_, manager) => manager.current != null,
|
||||
builder: (context, visible, child) {
|
||||
if (!visible) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (snapshot.playing && !_controller.isAnimating) {
|
||||
_controller.repeat();
|
||||
} else if (!snapshot.playing && _controller.isAnimating) {
|
||||
_controller.stop();
|
||||
}
|
||||
return PositionedDirectional(
|
||||
end: 8.w,
|
||||
top: 292.w,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => VoiceRoomRoute.openRoomMusic(context),
|
||||
child: RotationTransition(
|
||||
turns: _controller,
|
||||
child: Image.asset(
|
||||
"sc_images/room/sc_music_material_room_side.png",
|
||||
width: 58.w,
|
||||
height: 58.w,
|
||||
),
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: context.read<RoomMusicManager>().toggleRoomPlayerVisible,
|
||||
child: SizedBox(
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
child: Image.asset(
|
||||
"sc_images/room/sc_music_material_room_side.png",
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -68,20 +30,3 @@ class _RoomMusicFloatingEntryState extends State<RoomMusicFloatingEntry>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FloatingMusicSnapshot {
|
||||
const _FloatingMusicSnapshot({required this.visible, required this.playing});
|
||||
|
||||
final bool visible;
|
||||
final bool playing;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _FloatingMusicSnapshot &&
|
||||
other.visible == visible &&
|
||||
other.playing == playing;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(visible, playing);
|
||||
}
|
||||
|
||||
@ -74,9 +74,14 @@ class RoomMusicPlayerBar extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => manager.stop(clearCurrent: true),
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => manager.stop(clearCurrent: true),
|
||||
child: SizedBox(
|
||||
width: 40.w,
|
||||
height: 40.w,
|
||||
child: const Icon(Icons.close, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
251
lib/ui_kit/widgets/room/music/room_music_room_player.dart
Normal file
251
lib/ui_kit/widgets/room/music/room_music_room_player.dart
Normal file
@ -0,0 +1,251 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/modules/room/voice_room_route.dart';
|
||||
import 'package:yumi/services/music/room_music_manager.dart';
|
||||
|
||||
class RoomMusicRoomPlayer extends StatelessWidget {
|
||||
const RoomMusicRoomPlayer({super.key});
|
||||
|
||||
static const String _closeAsset =
|
||||
"sc_images/room/sc_music_room_player_close.png";
|
||||
static const String _minimizeAsset =
|
||||
"sc_images/room/sc_music_room_player_minimize.png";
|
||||
static const String _manageAsset =
|
||||
"sc_images/room/sc_music_room_player_manage.png";
|
||||
static const String _previousAsset =
|
||||
"sc_images/room/sc_music_room_player_previous.png";
|
||||
static const String _nextAsset =
|
||||
"sc_images/room/sc_music_room_player_next.png";
|
||||
static const String _playAsset = "sc_images/room/sc_music_material_play.png";
|
||||
static const String _pauseAsset =
|
||||
"sc_images/room/sc_music_material_pause.png";
|
||||
static const String _volumeAsset =
|
||||
"sc_images/room/sc_music_material_volume.png";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<RoomMusicManager>(
|
||||
builder: (context, manager, child) {
|
||||
final current = manager.current;
|
||||
if (current == null || !manager.roomPlayerVisible) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final duration = manager.durationMs <= 0 ? 1 : manager.durationMs;
|
||||
final position = manager.positionMs.clamp(0, duration).toInt();
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 10.w, 16.w, 10.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xE6072A24),
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.white.withValues(alpha: 0.10)),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_PlainIconButton(
|
||||
asset: _closeAsset,
|
||||
size: 24.w,
|
||||
onTap: () => manager.stop(clearCurrent: true),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
current.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
_PlainIconButton(
|
||||
asset: _minimizeAsset,
|
||||
size: 24.w,
|
||||
onTap: manager.hideRoomPlayer,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
_formatDuration(position),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
fontSize: 10.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: _MusicSlider(
|
||||
value: position.toDouble(),
|
||||
max: duration.toDouble(),
|
||||
onChanged: (value) => manager.seek(value.round()),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
_formatDuration(duration),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
fontSize: 10.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12.w),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_PlainIconButton(
|
||||
asset: _modeAsset(manager.playMode),
|
||||
size: 28.w,
|
||||
onTap: manager.switchPlayMode,
|
||||
),
|
||||
_PlainIconButton(
|
||||
asset: _previousAsset,
|
||||
size: 42.w,
|
||||
onTap: manager.previous,
|
||||
),
|
||||
_PlainIconButton(
|
||||
asset: manager.isPlaying ? _pauseAsset : _playAsset,
|
||||
size: 42.w,
|
||||
onTap: () {
|
||||
manager.isPlaying ? manager.pause() : manager.resume();
|
||||
},
|
||||
),
|
||||
_PlainIconButton(
|
||||
asset: _nextAsset,
|
||||
size: 42.w,
|
||||
onTap: manager.next,
|
||||
),
|
||||
_PlainIconButton(
|
||||
asset: _manageAsset,
|
||||
size: 28.w,
|
||||
onTap:
|
||||
() => VoiceRoomRoute.openRoomMusic(
|
||||
context,
|
||||
rootNavigator: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12.w),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(_volumeAsset, width: 22.w, height: 22.w),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: _MusicSlider(
|
||||
value: manager.volume.toDouble(),
|
||||
max: 100,
|
||||
onChanged: (value) => manager.setVolume(value.round()),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
SizedBox(
|
||||
width: 38.w,
|
||||
child: Text(
|
||||
"${manager.volume}%",
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
fontSize: 10.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static String _formatDuration(int milliseconds) {
|
||||
final totalSeconds = (milliseconds / 1000).floor();
|
||||
final minutes = totalSeconds ~/ 60;
|
||||
final seconds = totalSeconds % 60;
|
||||
return "$minutes:${seconds.toString().padLeft(2, '0')}";
|
||||
}
|
||||
|
||||
static String _modeAsset(RoomMusicPlayMode mode) {
|
||||
switch (mode) {
|
||||
case RoomMusicPlayMode.list:
|
||||
return "sc_images/room/sc_music_material_sequence.png";
|
||||
case RoomMusicPlayMode.shuffle:
|
||||
return "sc_images/room/sc_music_material_shuffle.png";
|
||||
case RoomMusicPlayMode.single:
|
||||
return "sc_images/room/sc_music_material_single_loop.png";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _MusicSlider extends StatelessWidget {
|
||||
const _MusicSlider({
|
||||
required this.value,
|
||||
required this.max,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final double value;
|
||||
final double max;
|
||||
final ValueChanged<double> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final safeMax = max <= 0 ? 1.0 : max;
|
||||
return SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 3.w,
|
||||
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 5.w),
|
||||
overlayShape: SliderComponentShape.noOverlay,
|
||||
activeTrackColor: const Color(0xFF33E6A2),
|
||||
inactiveTrackColor: Colors.white.withValues(alpha: 0.18),
|
||||
thumbColor: Colors.white,
|
||||
),
|
||||
child: Slider(
|
||||
min: 0,
|
||||
max: safeMax,
|
||||
value: value.clamp(0, safeMax).toDouble(),
|
||||
onChanged: onChanged,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlainIconButton extends StatelessWidget {
|
||||
const _PlainIconButton({
|
||||
required this.asset,
|
||||
required this.size,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String asset;
|
||||
final double size;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
width: size + 12.w,
|
||||
height: size + 12.w,
|
||||
child: Center(child: Image.asset(asset, width: size, height: size)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -198,7 +198,7 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset("sc_images/room/${item.icon}", width: 45.w, height: 45.w),
|
||||
_buildMenuIcon(item),
|
||||
SizedBox(height: 10.w),
|
||||
text(
|
||||
item.title,
|
||||
@ -292,6 +292,30 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuIcon(RoomMenu item) {
|
||||
if (item.id != _menuMusic) {
|
||||
return Image.asset(
|
||||
"sc_images/room/${item.icon}",
|
||||
width: 45.w,
|
||||
height: 45.w,
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: 45.w,
|
||||
height: 45.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.16),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Image.asset(
|
||||
"sc_images/room/${item.icon}",
|
||||
width: 26.w,
|
||||
height: 26.w,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomMenu {
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_banner_view.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_game_bottom_sheet.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/music/room_music_floating_entry.dart';
|
||||
|
||||
class RoomPlayWidget extends StatefulWidget {
|
||||
const RoomPlayWidget({super.key});
|
||||
@ -16,6 +17,11 @@ class _RoomPlayWidgetState extends State<RoomPlayWidget> {
|
||||
return SizedBox.expand(
|
||||
child: Stack(
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
end: 15.w,
|
||||
bottom: 150.w,
|
||||
child: const RoomMusicFloatingEntry(),
|
||||
),
|
||||
PositionedDirectional(
|
||||
end: 15.w,
|
||||
bottom: 100.w,
|
||||
|
||||
BIN
sc_images/room/sc_music_room_player_close.png
Normal file
BIN
sc_images/room/sc_music_room_player_close.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 588 B |
BIN
sc_images/room/sc_music_room_player_manage.png
Normal file
BIN
sc_images/room/sc_music_room_player_manage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 637 B |
BIN
sc_images/room/sc_music_room_player_minimize.png
Normal file
BIN
sc_images/room/sc_music_room_player_minimize.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 482 B |
BIN
sc_images/room/sc_music_room_player_next.png
Normal file
BIN
sc_images/room/sc_music_room_player_next.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 796 B |
BIN
sc_images/room/sc_music_room_player_previous.png
Normal file
BIN
sc_images/room/sc_music_room_player_previous.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 792 B |
BIN
sc_images/room/sc_music_user_bgm.png
Normal file
BIN
sc_images/room/sc_music_user_bgm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 730 B |
4
需求进度.md
4
需求进度.md
@ -15,8 +15,10 @@
|
||||
- 已接入语言房入口:房间底部菜单已恢复 `Music` 入口并跳转音乐页;语音房页面已挂载音乐悬浮入口,离开房间时会主动停止当前房间音乐,避免混音残留到房外页面。
|
||||
- 已按最新反馈调整房间菜单位置:`Music` 不再单独占用菜单第一行,而是并入常规菜单网格,紧跟在 `Background` 图标之后;背景入口功能开关保持打开状态。
|
||||
- 已修正本地音乐扫描器的兼容性风险:扫描 MP3 时不再直接读取 `SongModel.fileExtension/displayName/title` 等可能因 Android MediaStore 返回空值而触发异常的 getter,改为从插件原始 map 安全取值,并对文件存在性检查和扫描失败做兜底。
|
||||
- 已按最新音乐页与房间播放器设计继续对稿:音乐管理页右上角只保留添加按钮,排序/编辑按钮移到 `Total songs` 行末;空态不再展示搜索框,搜索框圆角收到 `17`;歌曲 item 改为 `#08251E` 背景和纵向渐隐描边,播放/暂停按钮移到右侧;左滑删除改为固定 `60` 宽删除按钮,需要点击删除按钮后才删除;音乐相关页面点击波浪反馈已收口移除。
|
||||
- 已重做房间内音乐入口与播放器:右侧音乐浮标不再旋转,并移动到游戏图标上方;点击浮标改为在房间内打开/隐藏播放器,不再直接进入歌曲管理页;房间播放器使用新增的关闭、最小化、上一首、下一首、进入管理页等素材;当前用户在麦上推送音乐时,自己麦位头像左下角会显示并旋转 `用户正在播放bgm` 标识。
|
||||
- 已补齐 `en/ar/tr/bn` 四套现有多语言文案;当前项目声明支持 `zh`,但仓库里暂无 `assets/l10n/intl_zh.json`,因此中文兜底仍由页面内 fallback 文案承担。
|
||||
- 验证进度:`flutter pub get` 已通过;新音乐相关目录和接线文件的定向 `flutter analyze` 无 error/warning,仅 `rtc_manager.dart` 保留项目原有 `avoid_print` info;全量 `flutter analyze` 仍被仓库既有测试依赖缺失和测试 API 问题阻断;`flutter build apk --debug` 已通过并生成 `build/app/outputs/flutter-apk/app-debug.apk`。
|
||||
- 验证进度:`flutter pub get` 已通过;音乐相关目录与房间接线文件的定向 `flutter analyze` 无 error/warning,当前仅 `rtc_manager.dart`、`sc_seat_item.dart` 保留项目原有 info 级别风格/废弃 API 提示;全量 `flutter analyze` 仍被仓库既有测试依赖缺失和测试 API 问题阻断;`flutter build apk --debug` 已通过并生成 `build/app/outputs/flutter-apk/app-debug.apk`。
|
||||
|
||||
## 本轮语言房进房加载逻辑优化(已完成)
|
||||
- 已按竞品加载思路梳理当前语言房进房链路:当前业务 `entryRoom` 成功后会先进入房间页,IM 加群、声网入会、麦位与在线用户刷新在房间页展示后继续进行;这与“IM 和声网都完成后才进入完整房间”的体验存在差异。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user