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 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的路由跳转工具类
|
/// fluro的路由跳转工具类
|
||||||
class SCNavigatorUtils {
|
class SCNavigatorUtils {
|
||||||
static bool inChatPage = false;
|
static bool inChatPage = false;
|
||||||
|
|||||||
@ -61,6 +61,14 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
|||||||
context,
|
context,
|
||||||
listen: false,
|
listen: false,
|
||||||
).initializeRealTimeCommunicationManager(context);
|
).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<RtmProvider>(context, listen: false).init(context);
|
||||||
Provider.of<SocialChatUserProfileManager>(
|
Provider.of<SocialChatUserProfileManager>(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@ -11,105 +11,115 @@ class RoomMusicFolderPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Theme(
|
||||||
backgroundColor: const Color(0xFF071F1B),
|
data: Theme.of(context).copyWith(
|
||||||
appBar: AppBar(
|
splashFactory: NoSplash.splashFactory,
|
||||||
backgroundColor: const Color(0xFF071F1B),
|
splashColor: Colors.transparent,
|
||||||
elevation: 0,
|
highlightColor: Colors.transparent,
|
||||||
title: Text(
|
|
||||||
RoomMusicTexts.t(context, "roomMusicAddMusic", "添加音乐"),
|
|
||||||
style: TextStyle(fontSize: 17.sp, fontWeight: FontWeight.w700),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
body: ListView.separated(
|
child: Scaffold(
|
||||||
padding: EdgeInsets.fromLTRB(16.w, 8.w, 16.w, 20.w),
|
backgroundColor: const Color(0xFF071F1B),
|
||||||
itemCount: folders.length,
|
appBar: AppBar(
|
||||||
separatorBuilder:
|
backgroundColor: const Color(0xFF071F1B),
|
||||||
(_, __) =>
|
elevation: 0,
|
||||||
Divider(color: Colors.white.withValues(alpha: 0.08), height: 1),
|
title: Text(
|
||||||
itemBuilder: (context, index) {
|
RoomMusicTexts.t(context, "roomMusicAddMusic", "添加音乐"),
|
||||||
final folder = folders[index];
|
style: TextStyle(fontSize: 17.sp, fontWeight: FontWeight.w700),
|
||||||
return InkWell(
|
),
|
||||||
onTap: () async {
|
),
|
||||||
final changed = await Navigator.of(context).push<bool>(
|
body: ListView.separated(
|
||||||
MaterialPageRoute(
|
padding: EdgeInsets.fromLTRB(16.w, 8.w, 16.w, 20.w),
|
||||||
builder: (_) => RoomMusicSelectPage(folder: folder),
|
itemCount: folders.length,
|
||||||
),
|
separatorBuilder:
|
||||||
);
|
(_, __) => Divider(
|
||||||
if (changed == true && context.mounted) {
|
color: Colors.white.withValues(alpha: 0.08),
|
||||||
Navigator.pop(context, true);
|
height: 1,
|
||||||
}
|
|
||||||
},
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Theme(
|
||||||
backgroundColor: const Color(0xFF071F1B),
|
data: Theme.of(context).copyWith(
|
||||||
body: SafeArea(
|
splashFactory: NoSplash.splashFactory,
|
||||||
child: Stack(
|
splashColor: Colors.transparent,
|
||||||
children: [
|
highlightColor: Colors.transparent,
|
||||||
Column(
|
),
|
||||||
children: [
|
child: Scaffold(
|
||||||
_buildHeader(context),
|
backgroundColor: const Color(0xFF071F1B),
|
||||||
_buildSearchField(),
|
body: SafeArea(
|
||||||
Expanded(
|
child: Stack(
|
||||||
child: Consumer<RoomMusicManager>(
|
children: [
|
||||||
builder: (context, manager, child) {
|
Column(
|
||||||
final songs = _filteredSongs(manager.playlist);
|
children: [
|
||||||
if (manager.playlist.isEmpty) {
|
_buildHeader(context),
|
||||||
return _buildEmptyState(context);
|
Expanded(
|
||||||
}
|
child: Consumer<RoomMusicManager>(
|
||||||
return Column(
|
builder: (context, manager, child) {
|
||||||
children: [
|
final songs = _filteredSongs(manager.playlist);
|
||||||
_buildTotal(context, manager.playlist.length),
|
if (manager.playlist.isEmpty) {
|
||||||
Expanded(
|
return _buildEmptyState(context);
|
||||||
child:
|
}
|
||||||
_isEditing && _searchController.text.isEmpty
|
return Column(
|
||||||
? _buildReorderableList(manager, songs)
|
children: [
|
||||||
: _buildSongList(manager, songs),
|
_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,
|
height: 54.w,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
_PlainIconButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
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(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -117,26 +130,14 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
_PlainIconButton(
|
||||||
onPressed: _startAddFlow,
|
onPressed: _startAddFlow,
|
||||||
icon: Image.asset(
|
child: Image.asset(
|
||||||
"sc_images/room/sc_icon_room_music_add.png",
|
"sc_images/room/sc_icon_room_music_add.png",
|
||||||
width: 24.w,
|
width: 24.w,
|
||||||
height: 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() {
|
Widget _buildSearchField() {
|
||||||
return Padding(
|
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(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
onChanged: (_) => setState(() {}),
|
onChanged: (_) => setState(() {}),
|
||||||
@ -164,7 +165,7 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
|||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white.withValues(alpha: 0.08),
|
fillColor: Colors.white.withValues(alpha: 0.08),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(18.w),
|
borderRadius: BorderRadius.circular(17.w),
|
||||||
borderSide: BorderSide.none,
|
borderSide: BorderSide.none,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -180,15 +181,39 @@ class _RoomMusicPageState extends State<RoomMusicPage> {
|
|||||||
);
|
);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 8.w),
|
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 8.w),
|
||||||
child: Align(
|
child: Row(
|
||||||
alignment: AlignmentDirectional.centerStart,
|
children: [
|
||||||
child: Text(
|
Expanded(
|
||||||
template.replaceAll("{1}", count.toString()),
|
child: Text(
|
||||||
style: TextStyle(
|
template.replaceAll("{1}", count.toString()),
|
||||||
color: Colors.white.withValues(alpha: 0.62),
|
style: TextStyle(
|
||||||
fontSize: 12.sp,
|
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),
|
SizedBox(height: 18.w),
|
||||||
ElevatedButton(
|
GestureDetector(
|
||||||
onPressed: _startAddFlow,
|
behavior: HitTestBehavior.opaque,
|
||||||
style: ElevatedButton.styleFrom(
|
onTap: _startAddFlow,
|
||||||
backgroundColor: const Color(0xFF33E6A2),
|
child: Container(
|
||||||
foregroundColor: const Color(0xFF06251F),
|
width: 164.w,
|
||||||
minimumSize: Size(164.w, 42.w),
|
height: 42.w,
|
||||||
shape: RoundedRectangleBorder(
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF33E6A2),
|
||||||
borderRadius: BorderRadius.circular(22.w),
|
borderRadius: BorderRadius.circular(22.w),
|
||||||
),
|
),
|
||||||
),
|
child: Text(
|
||||||
child: Text(
|
RoomMusicTexts.t(
|
||||||
RoomMusicTexts.t(context, "roomMusicScanFromPhone", "从手机扫描添加"),
|
context,
|
||||||
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w700),
|
"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,
|
itemCount: songs.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = songs[index];
|
final item = songs[index];
|
||||||
return Dismissible(
|
return Padding(
|
||||||
key: ValueKey("music-${item.id}"),
|
padding: EdgeInsets.only(bottom: 8.w),
|
||||||
direction: DismissDirection.endToStart,
|
child: _SwipeDeleteTile(
|
||||||
confirmDismiss: (_) => _confirmDelete(item),
|
key: ValueKey("music-${item.id}"),
|
||||||
background: Container(
|
onDelete: () {
|
||||||
alignment: AlignmentDirectional.centerEnd,
|
_confirmDelete(item);
|
||||||
padding: EdgeInsetsDirectional.only(end: 18.w),
|
},
|
||||||
color: const Color(0xFFE64A4A),
|
child: _SongTile(
|
||||||
child: Image.asset(
|
item: item,
|
||||||
"sc_images/room/sc_music_material_delete.png",
|
selected: manager.current?.id == item.id,
|
||||||
width: 24.w,
|
playing: manager.current?.id == item.id && manager.isPlaying,
|
||||||
height: 24.w,
|
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) {
|
itemBuilder: (context, index) {
|
||||||
final item = songs[index];
|
final item = songs[index];
|
||||||
return _SongTile(
|
return Padding(
|
||||||
key: ValueKey("reorder-${item.id}"),
|
key: ValueKey("reorder-${item.id}"),
|
||||||
item: item,
|
padding: EdgeInsets.only(bottom: 8.w),
|
||||||
selected: manager.current?.id == item.id,
|
child: _SongTile(
|
||||||
playing: manager.current?.id == item.id && manager.isPlaying,
|
item: item,
|
||||||
onTap: () => manager.play(item),
|
selected: manager.current?.id == item.id,
|
||||||
trailing: ReorderableDragStartListener(
|
playing: manager.current?.id == item.id && manager.isPlaying,
|
||||||
index: index,
|
onTap: () => manager.play(item),
|
||||||
child: Image.asset(
|
trailing: ReorderableDragStartListener(
|
||||||
"sc_images/room/sc_music_material_more.png",
|
index: index,
|
||||||
width: 24.w,
|
child: Image.asset(
|
||||||
height: 24.w,
|
"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 {
|
class _SongTile extends StatelessWidget {
|
||||||
const _SongTile({
|
const _SongTile({
|
||||||
super.key,
|
|
||||||
required this.item,
|
required this.item,
|
||||||
required this.selected,
|
required this.selected,
|
||||||
required this.playing,
|
required this.playing,
|
||||||
@ -405,57 +428,69 @@ class _SongTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InkWell(
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 62.w,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color:
|
gradient: const LinearGradient(
|
||||||
selected
|
begin: Alignment.topCenter,
|
||||||
? Colors.white.withValues(alpha: 0.08)
|
end: Alignment.bottomCenter,
|
||||||
: Colors.transparent,
|
colors: [Color(0xFFB2FBCC), Color(0x00B2FBCC)],
|
||||||
border: Border(
|
stops: [0, 1],
|
||||||
bottom: BorderSide(color: Colors.white.withValues(alpha: 0.08)),
|
|
||||||
),
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8.w),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Container(
|
||||||
children: [
|
height: 54.w,
|
||||||
Image.asset(
|
margin: const EdgeInsets.all(1),
|
||||||
playing
|
padding: EdgeInsetsDirectional.fromSTEB(12.w, 0, 12.w, 0),
|
||||||
? "sc_images/room/sc_music_material_pause.png"
|
decoration: BoxDecoration(
|
||||||
: "sc_images/room/sc_music_material_play.png",
|
color: const Color(0xFF08251E),
|
||||||
width: 28.w,
|
borderRadius: BorderRadius.circular(7.w),
|
||||||
height: 28.w,
|
),
|
||||||
),
|
child: Row(
|
||||||
SizedBox(width: 12.w),
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.title,
|
item.title,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: selected ? const Color(0xFF33E6A2) : Colors.white,
|
color:
|
||||||
fontSize: 14.sp,
|
selected ? const Color(0xFF33E6A2) : Colors.white,
|
||||||
fontWeight: FontWeight.w600,
|
fontSize: 14.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 5.w),
|
||||||
SizedBox(height: 5.w),
|
Text(
|
||||||
Text(
|
_formatDuration(item.durationMs),
|
||||||
_formatDuration(item.durationMs),
|
style: TextStyle(
|
||||||
style: TextStyle(
|
color: Colors.white.withValues(alpha: 0.52),
|
||||||
color: Colors.white.withValues(alpha: 0.52),
|
fontSize: 11.sp,
|
||||||
fontSize: 11.sp,
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
if (trailing != null) ...[
|
||||||
if (trailing != null) trailing!,
|
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')}";
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Theme(
|
||||||
backgroundColor: const Color(0xFF071F1B),
|
data: Theme.of(context).copyWith(
|
||||||
appBar: AppBar(
|
splashFactory: NoSplash.splashFactory,
|
||||||
backgroundColor: const Color(0xFF071F1B),
|
splashColor: Colors.transparent,
|
||||||
elevation: 0,
|
highlightColor: Colors.transparent,
|
||||||
title: Text(
|
|
||||||
RoomMusicTexts.t(context, "roomMusicAddMusic", "添加音乐"),
|
|
||||||
style: TextStyle(fontSize: 17.sp, fontWeight: FontWeight.w700),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
body: Column(
|
child: Scaffold(
|
||||||
children: [
|
backgroundColor: const Color(0xFF071F1B),
|
||||||
Expanded(
|
appBar: AppBar(
|
||||||
child: ListView.separated(
|
backgroundColor: const Color(0xFF071F1B),
|
||||||
padding: EdgeInsets.fromLTRB(16.w, 8.w, 16.w, 16.w),
|
elevation: 0,
|
||||||
itemCount: widget.folder.songs.length,
|
title: Text(
|
||||||
separatorBuilder:
|
RoomMusicTexts.t(context, "roomMusicAddMusic", "添加音乐"),
|
||||||
(_, __) => Divider(
|
style: TextStyle(fontSize: 17.sp, fontWeight: FontWeight.w700),
|
||||||
color: Colors.white.withValues(alpha: 0.08),
|
|
||||||
height: 1,
|
|
||||||
),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return _buildSongItem(widget.folder.songs[index]);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
_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)
|
? Colors.white.withValues(alpha: 0.34)
|
||||||
: const Color(0xFF33E6A2);
|
: const Color(0xFF33E6A2);
|
||||||
|
|
||||||
return InkWell(
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@ -161,7 +169,8 @@ class _RoomMusicSelectPageState extends State<RoomMusicSelectPage> {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_allSelected) {
|
if (_allSelected) {
|
||||||
@ -193,21 +202,31 @@ class _RoomMusicSelectPageState extends State<RoomMusicSelectPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
ElevatedButton(
|
GestureDetector(
|
||||||
onPressed: _hasChanged ? _submit : null,
|
behavior: HitTestBehavior.opaque,
|
||||||
style: ElevatedButton.styleFrom(
|
onTap: _hasChanged ? _submit : null,
|
||||||
backgroundColor: const Color(0xFF33E6A2),
|
child: Container(
|
||||||
disabledBackgroundColor: Colors.white.withValues(alpha: 0.18),
|
width: 104.w,
|
||||||
foregroundColor: const Color(0xFF06251F),
|
height: 40.w,
|
||||||
disabledForegroundColor: Colors.white.withValues(alpha: 0.38),
|
alignment: Alignment.center,
|
||||||
shape: RoundedRectangleBorder(
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
_hasChanged
|
||||||
|
? const Color(0xFF33E6A2)
|
||||||
|
: Colors.white.withValues(alpha: 0.18),
|
||||||
borderRadius: BorderRadius.circular(20.w),
|
borderRadius: BorderRadius.circular(20.w),
|
||||||
),
|
),
|
||||||
minimumSize: Size(104.w, 40.w),
|
child: Text(
|
||||||
),
|
RoomMusicTexts.t(context, "add", "添加"),
|
||||||
child: Text(
|
style: TextStyle(
|
||||||
RoomMusicTexts.t(context, "add", "添加"),
|
color:
|
||||||
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w700),
|
_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/app/constants/sc_screen.dart';
|
||||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||||
import 'package:yumi/services/audio/rtc_manager.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';
|
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,
|
width: widget.isGameModel ? 38.w : 52.w,
|
||||||
height: 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(
|
Positioned(
|
||||||
bottom: widget.isGameModel ? 2.w : 5.w,
|
bottom: widget.isGameModel ? 2.w : 5.w,
|
||||||
right: 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 {
|
class _SeatRenderSnapshot {
|
||||||
const _SeatRenderSnapshot({
|
const _SeatRenderSnapshot({
|
||||||
required this.isExitingCurrentVoiceRoomSession,
|
required this.isExitingCurrentVoiceRoomSession,
|
||||||
@ -267,6 +333,7 @@ class _SeatRenderSnapshot {
|
|||||||
required this.userHeartbeatValue,
|
required this.userHeartbeatValue,
|
||||||
required this.micLock,
|
required this.micLock,
|
||||||
required this.micMute,
|
required this.micMute,
|
||||||
|
required this.isCurrentUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory _SeatRenderSnapshot.fromProvider(RtcProvider provider, num index) {
|
factory _SeatRenderSnapshot.fromProvider(RtcProvider provider, num index) {
|
||||||
@ -287,6 +354,7 @@ class _SeatRenderSnapshot {
|
|||||||
userHeartbeatValue: user?.heartbeatVal ?? 0,
|
userHeartbeatValue: user?.heartbeatVal ?? 0,
|
||||||
micLock: roomSeat?.micLock ?? false,
|
micLock: roomSeat?.micLock ?? false,
|
||||||
micMute: roomSeat?.micMute ?? false,
|
micMute: roomSeat?.micMute ?? false,
|
||||||
|
isCurrentUser: provider.isOnMaiInIndex(index),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,6 +369,7 @@ class _SeatRenderSnapshot {
|
|||||||
final num userHeartbeatValue;
|
final num userHeartbeatValue;
|
||||||
final bool micLock;
|
final bool micLock;
|
||||||
final bool micMute;
|
final bool micMute;
|
||||||
|
final bool isCurrentUser;
|
||||||
|
|
||||||
bool get hasUser => userId.isNotEmpty;
|
bool get hasUser => userId.isNotEmpty;
|
||||||
|
|
||||||
@ -321,7 +390,8 @@ class _SeatRenderSnapshot {
|
|||||||
other.userVipName == userVipName &&
|
other.userVipName == userVipName &&
|
||||||
other.userHeartbeatValue == userHeartbeatValue &&
|
other.userHeartbeatValue == userHeartbeatValue &&
|
||||||
other.micLock == micLock &&
|
other.micLock == micLock &&
|
||||||
other.micMute == micMute;
|
other.micMute == micMute &&
|
||||||
|
other.isCurrentUser == isCurrentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -337,6 +407,7 @@ class _SeatRenderSnapshot {
|
|||||||
userHeartbeatValue,
|
userHeartbeatValue,
|
||||||
micLock,
|
micLock,
|
||||||
micMute,
|
micMute,
|
||||||
|
isCurrentUser,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.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_msg_item.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/room_online_user_widget.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/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 'package:yumi/shared/data_sources/models/enum/sc_gift_type.dart';
|
||||||
|
|
||||||
import '../../ui_kit/components/sc_float_ichart.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);
|
_tabController = TabController(length: _pages.length, vsync: this);
|
||||||
_enableRoomVisualEffects();
|
_enableRoomVisualEffects();
|
||||||
_tabController.addListener(_handleTabChange);
|
_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((
|
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
|
||||||
event,
|
event,
|
||||||
) {
|
) {
|
||||||
@ -179,24 +169,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
if (snapshot.status != RoomStartupStatus.loading) {
|
if (snapshot.status != RoomStartupStatus.loading) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
return Positioned.fill(
|
return const SizedBox.shrink();
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -329,7 +302,12 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
_buildRoomStartupLayer(),
|
_buildRoomStartupLayer(),
|
||||||
const RoomMusicFloatingEntry(),
|
const PositionedDirectional(
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: RoomMusicRoomPlayer(),
|
||||||
|
),
|
||||||
// _buildPlayViews(),
|
// _buildPlayViews(),
|
||||||
///幸运礼物中奖动画
|
///幸运礼物中奖动画
|
||||||
LuckGiftNomorAnimWidget(),
|
LuckGiftNomorAnimWidget(),
|
||||||
|
|||||||
@ -21,11 +21,27 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
|||||||
static String roomBackgroundUpload = '/room/background/upload';
|
static String roomBackgroundUpload = '/room/background/upload';
|
||||||
static String roomMusic = '/room/music';
|
static String roomMusic = '/room/music';
|
||||||
|
|
||||||
static Route<T> _buildRoute<T>(Widget page) {
|
static Route<T> _buildRoute<T>(Widget page, {RouteSettings? settings}) {
|
||||||
if (Platform.isIOS) {
|
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>(
|
static Future<T?> openRoomEdit<T>(
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||||
import 'package:fluro/fluro.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:yumi/app_localizations.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_routes.dart';
|
||||||
import 'package:yumi/app/routes/sc_fluro_navigator.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_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/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/repositories/sc_user_repository_impl.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.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 }
|
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 {
|
class RealTimeCommunicationManager extends ChangeNotifier {
|
||||||
static const Duration _micListPollingInterval = Duration(seconds: 2);
|
static const Duration _micListPollingInterval = Duration(seconds: 2);
|
||||||
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
|
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 _selfMicStateGracePeriod = Duration(seconds: 4);
|
||||||
static const Duration _giftTriggeredMicRefreshMinInterval = Duration(
|
static const Duration _giftTriggeredMicRefreshMinInterval = Duration(
|
||||||
milliseconds: 900,
|
milliseconds: 900,
|
||||||
@ -69,6 +93,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
bool _isHandlingAgoraRoomFailure = false;
|
bool _isHandlingAgoraRoomFailure = false;
|
||||||
bool _agoraJoined = false;
|
bool _agoraJoined = false;
|
||||||
bool _agoraRoleIsBroadcaster = false;
|
bool _agoraRoleIsBroadcaster = false;
|
||||||
|
bool _agoraAudioConfigured = false;
|
||||||
RoomStartupStatus _roomStartupStatus = RoomStartupStatus.idle;
|
RoomStartupStatus _roomStartupStatus = RoomStartupStatus.idle;
|
||||||
RoomStartupFailureType _roomStartupFailureType = RoomStartupFailureType.none;
|
RoomStartupFailureType _roomStartupFailureType = RoomStartupFailureType.none;
|
||||||
int _roomStartupFailureToken = 0;
|
int _roomStartupFailureToken = 0;
|
||||||
@ -78,6 +103,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
Timer? _joinAgoraTimeoutTimer;
|
Timer? _joinAgoraTimeoutTimer;
|
||||||
Timer? _agoraDisconnectedCleanupTimer;
|
Timer? _agoraDisconnectedCleanupTimer;
|
||||||
Completer<void>? _joinAgoraCompleter;
|
Completer<void>? _joinAgoraCompleter;
|
||||||
|
Future<RtcEngine>? _rtcEngineInitTask;
|
||||||
|
Future<void>? _rtcEnginePrewarmTask;
|
||||||
String? _joiningChannelId;
|
String? _joiningChannelId;
|
||||||
bool _isRefreshingMicList = false;
|
bool _isRefreshingMicList = false;
|
||||||
bool _isRefreshingOnlineUsers = false;
|
bool _isRefreshingOnlineUsers = false;
|
||||||
@ -948,6 +975,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
_resyncHeartbeatFromAgoraState();
|
_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(
|
Future<void> _handleAgoraJoinFailed(
|
||||||
Object error, {
|
Object error, {
|
||||||
required bool exitRoom,
|
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();
|
final channelId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||||
if (channelId.isEmpty) {
|
if (channelId.isEmpty) {
|
||||||
final error = StateError('Agora channel id is empty');
|
final error = StateError('Agora channel id is empty');
|
||||||
@ -1013,23 +1101,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final rtcEngine = await _initAgoraRtcEngine();
|
final rtcEngine = await _initAgoraRtcEngine();
|
||||||
rtcEngine.setAudioProfile(
|
await _configureAgoraAudioEngine(rtcEngine);
|
||||||
profile: AudioProfileType.audioProfileSpeechStandard,
|
final resolvedRtcToken =
|
||||||
scenario: AudioScenarioType.audioScenarioGameStreaming,
|
rtcToken ?? await _fetchRtcTokenForCurrentRoom(isPublisher: false);
|
||||||
);
|
|
||||||
rtcEngine.enableAudioVolumeIndication(
|
|
||||||
interval: 500,
|
|
||||||
smooth: 3,
|
|
||||||
reportVad: true,
|
|
||||||
);
|
|
||||||
await rtcEngine.disableVideo();
|
|
||||||
var rtcToken = await SCAccountRepository().getRtcToken(
|
|
||||||
channelId,
|
|
||||||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
|
|
||||||
isPublisher: false,
|
|
||||||
);
|
|
||||||
await rtcEngine.joinChannel(
|
await rtcEngine.joinChannel(
|
||||||
token: rtcToken.rtcToken ?? "",
|
token: resolvedRtcToken,
|
||||||
channelId: channelId,
|
channelId: channelId,
|
||||||
uid: _resolveAgoraUidForCurrentUser(),
|
uid: _resolveAgoraUidForCurrentUser(),
|
||||||
options: const ChannelMediaOptions(
|
options: const ChannelMediaOptions(
|
||||||
@ -1054,7 +1130,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
if (throwOnError) {
|
if (throwOnError) {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
print('加入失败:${e.runtimeType},${e.toString()}');
|
debugPrint('加入失败:${e.runtimeType},${e.toString()}');
|
||||||
} finally {
|
} finally {
|
||||||
if (identical(_joinAgoraCompleter, joinCompleter)) {
|
if (identical(_joinAgoraCompleter, joinCompleter)) {
|
||||||
_cancelAgoraJoinWaiter();
|
_cancelAgoraJoinWaiter();
|
||||||
@ -1165,10 +1241,32 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<RtcEngine> _initAgoraRtcEngine() async {
|
Future<RtcEngine> _initAgoraRtcEngine() {
|
||||||
if (engine != null) {
|
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();
|
final rtcEngine = createAgoraRtcEngine();
|
||||||
await rtcEngine.initialize(
|
await rtcEngine.initialize(
|
||||||
RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid),
|
RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid),
|
||||||
@ -1176,7 +1274,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
engine = rtcEngine;
|
engine = rtcEngine;
|
||||||
_rtcEngineEventHandler = RtcEngineEventHandler(
|
_rtcEngineEventHandler = RtcEngineEventHandler(
|
||||||
onError: (ErrorCodeType err, String msg) {
|
onError: (ErrorCodeType err, String msg) {
|
||||||
print('rtc错误$err');
|
debugPrint('rtc错误$err');
|
||||||
final error = 'Agora error: $err $msg';
|
final error = 'Agora error: $err $msg';
|
||||||
if (!_agoraJoined) {
|
if (!_agoraJoined) {
|
||||||
final completer = _joinAgoraCompleter;
|
final completer = _joinAgoraCompleter;
|
||||||
@ -1192,7 +1290,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
|
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
|
||||||
print('rtc 自己加入 ${connection.channelId} ${connection.localUid}');
|
debugPrint('rtc 自己加入 ${connection.channelId} ${connection.localUid}');
|
||||||
final joinedChannelId = connection.channelId ?? "";
|
final joinedChannelId = connection.channelId ?? "";
|
||||||
if (!_isCurrentAgoraChannel(joinedChannelId)) {
|
if (!_isCurrentAgoraChannel(joinedChannelId)) {
|
||||||
return;
|
return;
|
||||||
@ -1309,11 +1407,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
_roomMusicMixingStateListener?.call(state, reason);
|
_roomMusicMixingStateListener?.call(state, reason);
|
||||||
},
|
},
|
||||||
onUserJoined: (connection, remoteUid, elapsed) {
|
onUserJoined: (connection, remoteUid, elapsed) {
|
||||||
print('rtc用户 $remoteUid 加入了频道');
|
debugPrint('rtc用户 $remoteUid 加入了频道');
|
||||||
},
|
},
|
||||||
// 监听远端用户离开
|
// 监听远端用户离开
|
||||||
onUserOffline: (connection, remoteUid, reason) {
|
onUserOffline: (connection, remoteUid, reason) {
|
||||||
print('rtc用户 $remoteUid 离开了频道 (原因: $reason)');
|
debugPrint('rtc用户 $remoteUid 离开了频道 (原因: $reason)');
|
||||||
},
|
},
|
||||||
onTokenPrivilegeWillExpire: (
|
onTokenPrivilegeWillExpire: (
|
||||||
RtcConnection connection,
|
RtcConnection connection,
|
||||||
@ -1329,9 +1427,27 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
onAudioVolumeIndication: initializeAudioVolumeIndicationCallback,
|
onAudioVolumeIndication: initializeAudioVolumeIndicationCallback,
|
||||||
);
|
);
|
||||||
rtcEngine.registerEventHandler(_rtcEngineEventHandler!);
|
rtcEngine.registerEventHandler(_rtcEngineEventHandler!);
|
||||||
|
await _configureAgoraAudioEngine(rtcEngine);
|
||||||
return 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 {
|
Future<void> releaseRtcEngineForAppTermination() async {
|
||||||
final rtcEngine = engine;
|
final rtcEngine = engine;
|
||||||
if (rtcEngine == null) {
|
if (rtcEngine == null) {
|
||||||
@ -1341,6 +1457,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
final eventHandler = _rtcEngineEventHandler;
|
final eventHandler = _rtcEngineEventHandler;
|
||||||
engine = null;
|
engine = null;
|
||||||
_rtcEngineEventHandler = null;
|
_rtcEngineEventHandler = null;
|
||||||
|
_rtcEngineInitTask = null;
|
||||||
|
_rtcEnginePrewarmTask = null;
|
||||||
|
_agoraAudioConfigured = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (eventHandler != null) {
|
if (eventHandler != null) {
|
||||||
@ -1348,14 +1467,14 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
await rtcEngine.leaveChannel();
|
await rtcEngine.leaveChannel();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('rtc销毁前离开频道出错: $e');
|
debugPrint('rtc销毁前离开频道出错: $e');
|
||||||
}
|
}
|
||||||
_resetAgoraTracking();
|
_resetAgoraTracking();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await rtcEngine.release();
|
await rtcEngine.release();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('rtc释放引擎出错: $e');
|
debugPrint('rtc释放引擎出错: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1506,35 +1625,16 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
_setRoomStartupReady();
|
_setRoomStartupReady();
|
||||||
setRoomVisualEffectsEnabled(true);
|
setRoomVisualEffectsEnabled(true);
|
||||||
SCFloatIchart().remove();
|
SCFloatIchart().remove();
|
||||||
SCNavigatorUtils.push(
|
VoiceRoomRoute.openVoiceRoom(context);
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
SCFloatIchart().remove();
|
SCFloatIchart().remove();
|
||||||
if (currenRoom != null) {
|
if (currenRoom != null) {
|
||||||
await exitCurrentVoiceRoomSession(false);
|
await exitCurrentVoiceRoomSession(false, deferRemoteCleanup: false);
|
||||||
}
|
}
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||||
SCLoadingManager.show(context: context);
|
|
||||||
try {
|
try {
|
||||||
currenRoom = await SCAccountRepository().entryRoom(
|
currenRoom = await SCAccountRepository().entryRoom(
|
||||||
roomId,
|
roomId,
|
||||||
@ -1550,8 +1650,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
await resetLocalRoomState(fallbackRtmProvider: rtmProvider);
|
await resetLocalRoomState(fallbackRtmProvider: rtmProvider);
|
||||||
rethrow;
|
rethrow;
|
||||||
} finally {
|
|
||||||
SCLoadingManager.hide();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1586,15 +1684,22 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
_setRoomStartupLoading();
|
_setRoomStartupLoading();
|
||||||
setRoomVisualEffectsEnabled(true);
|
setRoomVisualEffectsEnabled(true);
|
||||||
SCNavigatorUtils.push(context!, VoiceRoomRoute.voiceRoom, replace: false);
|
VoiceRoomRoute.openVoiceRoom(context!);
|
||||||
unawaited(_bootstrapEnteredVoiceRoomSession());
|
unawaited(_bootstrapEnteredVoiceRoomSession());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _bootstrapEnteredVoiceRoomSession() async {
|
Future<void> _bootstrapEnteredVoiceRoomSession() async {
|
||||||
var failureType = RoomStartupFailureType.im;
|
var failureType = RoomStartupFailureType.im;
|
||||||
try {
|
try {
|
||||||
|
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||||
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
|
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) {
|
if (joinResult == null || joinResult.code != 0) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[RoomStartup] join IM group failed code=${joinResult?.code} desc=${joinResult?.desc}',
|
'[RoomStartup] join IM group failed code=${joinResult?.code} desc=${joinResult?.desc}',
|
||||||
@ -1618,19 +1723,16 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
///获取麦位
|
await Future.wait([microphoneListFuture, onlineUsersFuture]);
|
||||||
retrieveMicrophoneList();
|
|
||||||
fetchOnlineUsersList();
|
|
||||||
_startRoomStatePolling();
|
_startRoomStatePolling();
|
||||||
Provider.of<SocialChatRoomManager>(
|
Provider.of<SocialChatRoomManager>(
|
||||||
context!,
|
context!,
|
||||||
listen: false,
|
listen: false,
|
||||||
).fetchContributionLevelData(
|
).fetchContributionLevelData(roomId);
|
||||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
|
||||||
);
|
|
||||||
|
|
||||||
failureType = RoomStartupFailureType.rtc;
|
failureType = RoomStartupFailureType.rtc;
|
||||||
await joinAgoraVoiceChannel(throwOnError: true);
|
final rtcToken = (await rtcTokenFuture).requireValue();
|
||||||
|
await joinAgoraVoiceChannel(throwOnError: true, rtcToken: rtcToken);
|
||||||
await _bootstrapRoomHeartbeatState();
|
await _bootstrapRoomHeartbeatState();
|
||||||
_setRoomStartupReady();
|
_setRoomStartupReady();
|
||||||
|
|
||||||
@ -1818,40 +1920,141 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future exitCurrentVoiceRoomSession(bool isLogout) async {
|
Future exitCurrentVoiceRoomSession(
|
||||||
|
bool isLogout, {
|
||||||
|
bool deferRemoteCleanup = true,
|
||||||
|
}) async {
|
||||||
|
if (_isExitingCurrentVoiceRoomSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_setExitingCurrentVoiceRoomSession(true);
|
_setExitingCurrentVoiceRoomSession(true);
|
||||||
_stopRoomStatePolling();
|
_stopRoomStatePolling();
|
||||||
|
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
|
||||||
|
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||||
|
final exitRtmProvider = rtmProvider;
|
||||||
|
final navigationContext = context;
|
||||||
try {
|
try {
|
||||||
rtmProvider?.msgAllListener = null;
|
exitRtmProvider?.msgAllListener = null;
|
||||||
rtmProvider?.msgChatListener = null;
|
exitRtmProvider?.msgChatListener = null;
|
||||||
rtmProvider?.msgGiftListener = null;
|
exitRtmProvider?.msgGiftListener = null;
|
||||||
SCLoadingManager.show(context: context);
|
|
||||||
SCGiftVapSvgaManager().stopPlayback();
|
SCGiftVapSvgaManager().stopPlayback();
|
||||||
setRoomVisualEffectsEnabled(false);
|
setRoomVisualEffectsEnabled(false);
|
||||||
SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false);
|
SCHeartbeatUtils.cancelTimer();
|
||||||
SCHeartbeatUtils.cancelAnchorTimer();
|
exitRtmProvider?.cleanRoomData();
|
||||||
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));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('rtc退出房间出错: $e');
|
debugPrint('rtc退出房间本地清理出错: $e');
|
||||||
_clearData();
|
}
|
||||||
SCLoadingManager.hide();
|
|
||||||
if (!isLogout) {
|
_clearData();
|
||||||
SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home));
|
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 {
|
Future<void> resetLocalRoomState({RtmProvider? fallbackRtmProvider}) async {
|
||||||
@ -1867,7 +2070,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
await _cleanupAgoraRoomState();
|
await _cleanupAgoraRoomState();
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('rtc清理本地房间状态出错: $e');
|
debugPrint('rtc清理本地房间状态出错: $e');
|
||||||
} finally {
|
} finally {
|
||||||
rtmProvider?.cleanRoomData();
|
rtmProvider?.cleanRoomData();
|
||||||
_clearData();
|
_clearData();
|
||||||
@ -1903,7 +2106,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
await SCAccountRepository().quitRoom(roomId);
|
await SCAccountRepository().quitRoom(roomId);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('rtc进房失败清理本地房间状态出错: $e');
|
debugPrint('rtc进房失败清理本地房间状态出错: $e');
|
||||||
} finally {
|
} finally {
|
||||||
_clearData();
|
_clearData();
|
||||||
_isHandlingRoomStartupFailure = false;
|
_isHandlingRoomStartupFailure = false;
|
||||||
@ -2098,12 +2301,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
|
|
||||||
addSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) {
|
addSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) {
|
||||||
_onSoundVoiceChangeList.add(onSoundVoiceChange);
|
_onSoundVoiceChangeList.add(onSoundVoiceChange);
|
||||||
print('添加监听:${_onSoundVoiceChangeList.length}');
|
debugPrint('添加监听:${_onSoundVoiceChangeList.length}');
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) {
|
removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) {
|
||||||
_onSoundVoiceChangeList.remove(onSoundVoiceChange);
|
_onSoundVoiceChangeList.remove(onSoundVoiceChange);
|
||||||
print('删除监听:${_onSoundVoiceChangeList.length}');
|
debugPrint('删除监听:${_onSoundVoiceChangeList.length}');
|
||||||
}
|
}
|
||||||
|
|
||||||
void shangMai(num index, {String? eventType, String? inviterId}) async {
|
void shangMai(num index, {String? eventType, String? inviterId}) async {
|
||||||
@ -2286,7 +2489,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
SCTts.show("kill the user fail");
|
SCTts.show("kill the user fail");
|
||||||
print('踢人下麦失败');
|
debugPrint('踢人下麦失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,7 @@ class RoomMusicManager extends ChangeNotifier {
|
|||||||
bool _isStarting = false;
|
bool _isStarting = false;
|
||||||
bool _isStoppingByUser = false;
|
bool _isStoppingByUser = false;
|
||||||
bool _lastLoopback = true;
|
bool _lastLoopback = true;
|
||||||
|
bool _roomPlayerVisible = false;
|
||||||
|
|
||||||
List<SCMusicMode> get playlist => List.unmodifiable(_playlist);
|
List<SCMusicMode> get playlist => List.unmodifiable(_playlist);
|
||||||
|
|
||||||
@ -56,6 +57,10 @@ class RoomMusicManager extends ChangeNotifier {
|
|||||||
|
|
||||||
bool get hasCurrent => _current != null;
|
bool get hasCurrent => _current != null;
|
||||||
|
|
||||||
|
bool get roomPlayerVisible => _roomPlayerVisible;
|
||||||
|
|
||||||
|
bool get isPublishingToRoom => _isPlaying && !_lastLoopback;
|
||||||
|
|
||||||
int get currentIndex {
|
int get currentIndex {
|
||||||
final currentId = _current?.id;
|
final currentId = _current?.id;
|
||||||
if (currentId == null) {
|
if (currentId == null) {
|
||||||
@ -144,6 +149,7 @@ class RoomMusicManager extends ChangeNotifier {
|
|||||||
_durationMs = item.durationMs;
|
_durationMs = item.durationMs;
|
||||||
_positionMs = startPositionMs;
|
_positionMs = startPositionMs;
|
||||||
_isPaused = false;
|
_isPaused = false;
|
||||||
|
_roomPlayerVisible = true;
|
||||||
await _startCurrent(startPositionMs: startPositionMs);
|
await _startCurrent(startPositionMs: startPositionMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,6 +194,7 @@ class RoomMusicManager extends ChangeNotifier {
|
|||||||
if (clearCurrent) {
|
if (clearCurrent) {
|
||||||
_current = null;
|
_current = null;
|
||||||
_durationMs = 0;
|
_durationMs = 0;
|
||||||
|
_roomPlayerVisible = false;
|
||||||
}
|
}
|
||||||
notifyListeners();
|
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 {
|
Future<void> syncPlaybackRouting() async {
|
||||||
final current = _current;
|
final current = _current;
|
||||||
final rtcProvider = _rtcProvider;
|
final rtcProvider = _rtcProvider;
|
||||||
@ -430,6 +453,7 @@ class RoomMusicManager extends ChangeNotifier {
|
|||||||
_current = null;
|
_current = null;
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
_isPaused = false;
|
_isPaused = false;
|
||||||
|
_roomPlayerVisible = false;
|
||||||
_positionMs = 0;
|
_positionMs = 0;
|
||||||
_durationMs = 0;
|
_durationMs = 0;
|
||||||
_stopPositionTimer();
|
_stopPositionTimer();
|
||||||
|
|||||||
@ -17,6 +17,7 @@ class SCHeartbeatUtils {
|
|||||||
String status,
|
String status,
|
||||||
bool upMick, {
|
bool upMick, {
|
||||||
String? roomId,
|
String? roomId,
|
||||||
|
bool rethrowOnError = false,
|
||||||
}) async {
|
}) async {
|
||||||
cancelTimer();
|
cancelTimer();
|
||||||
_c = status;
|
_c = status;
|
||||||
@ -40,6 +41,9 @@ class SCHeartbeatUtils {
|
|||||||
try {
|
try {
|
||||||
cancelTimer();
|
cancelTimer();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
if (rethrowOnError) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -109,10 +109,7 @@ class SCChatRoomHelper {
|
|||||||
).retrieveMicrophoneList();
|
).retrieveMicrophoneList();
|
||||||
Provider.of<RealTimeCommunicationManager>(context, listen: false)
|
Provider.of<RealTimeCommunicationManager>(context, listen: false)
|
||||||
.closeFullGame = true;
|
.closeFullGame = true;
|
||||||
SCNavigatorUtils.push(
|
VoiceRoomRoute.openVoiceRoom(context);
|
||||||
context,
|
|
||||||
'${VoiceRoomRoute.voiceRoom}?id=${Provider.of<RealTimeCommunicationManager>(context, listen: false).currenRoom?.roomProfile?.roomProfile?.id}',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static double getCurrenProgress(
|
static double getCurrenProgress(
|
||||||
|
|||||||
@ -1,133 +1,96 @@
|
|||||||
import 'dart:convert';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:yumi/app_localizations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||||
import 'package:yumi/app_localizations.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
import 'package:yumi/app/routes/sc_routes.dart';
|
||||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
import 'package:yumi/services/gift/gift_animation_manager.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
|
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:yumi/app/routes/sc_routes.dart';
|
import '../../components/sc_float_ichart.dart';
|
||||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
class ExitMinRoomPage extends StatefulWidget {
|
||||||
import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart';
|
final bool isFz;
|
||||||
import 'package:yumi/services/gift/gift_animation_manager.dart';
|
final String roomId;
|
||||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
|
||||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
const ExitMinRoomPage(this.isFz, this.roomId, {super.key});
|
||||||
import 'package:yumi/modules/index/main_route.dart';
|
|
||||||
|
@override
|
||||||
import '../../components/sc_float_ichart.dart';
|
State<ExitMinRoomPage> createState() => _ExitMinRoomPageState();
|
||||||
|
}
|
||||||
class ExitMinRoomPage extends StatefulWidget {
|
|
||||||
bool isFz;
|
class _ExitMinRoomPageState extends State<ExitMinRoomPage> {
|
||||||
String roomId;
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
ExitMinRoomPage(this.isFz, this.roomId);
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
@override
|
children: [
|
||||||
_ExitMinRoomPageState createState() => _ExitMinRoomPageState();
|
SCDebounceWidget(
|
||||||
}
|
child: Container(color: Colors.transparent),
|
||||||
|
onTap: () {
|
||||||
class _ExitMinRoomPageState extends State<ExitMinRoomPage> {
|
Navigator.of(context).pop();
|
||||||
SCUserIdentityRes? userIdentity;
|
},
|
||||||
bool isLoading = true;
|
),
|
||||||
|
Column(
|
||||||
@override
|
spacing: 55.w,
|
||||||
void initState() {
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
super.initState();
|
children: [
|
||||||
SCAccountRepository()
|
SCDebounceWidget(
|
||||||
.userIdentity(
|
child: Column(
|
||||||
userId:
|
children: [
|
||||||
Provider.of<RtcProvider>(
|
Image.asset(
|
||||||
context,
|
"sc_images/room/sc_icon_min_room.png",
|
||||||
listen: false,
|
width: 48.w,
|
||||||
).currenRoom?.roomProfile?.roomProfile?.userId,
|
height: 48.w,
|
||||||
)
|
),
|
||||||
.then((v) {
|
SizedBox(height: 8.w),
|
||||||
userIdentity = v;
|
text(
|
||||||
isLoading = false;
|
SCAppLocalizations.of(context)!.mInimize,
|
||||||
setState(() {});
|
fontSize: 15.sp,
|
||||||
})
|
textColor: Colors.white,
|
||||||
.catchError((e) {
|
fontWeight: FontWeight.w500,
|
||||||
isLoading = false;
|
),
|
||||||
setState(() {});
|
],
|
||||||
});
|
),
|
||||||
}
|
onTap: () {
|
||||||
|
final giftAnimationManager =
|
||||||
@override
|
context.read<GiftAnimationManager>();
|
||||||
Widget build(BuildContext context) {
|
SCFloatIchart().show();
|
||||||
return Stack(
|
SCNavigatorUtils.popUntil(
|
||||||
alignment: Alignment.center,
|
context,
|
||||||
children: [
|
ModalRoute.withName(SCRoutes.home),
|
||||||
SCDebounceWidget(
|
);
|
||||||
child: Container(color: Colors.transparent),
|
giftAnimationManager.cleanupAnimationResources();
|
||||||
onTap: () {
|
},
|
||||||
Navigator.of(context).pop();
|
),
|
||||||
},
|
SCDebounceWidget(
|
||||||
),
|
child: Column(
|
||||||
Column(
|
children: [
|
||||||
spacing: 55.w,
|
Image.asset(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
"sc_images/room/sc_icon_exit_room.png",
|
||||||
children: [
|
width: 48.w,
|
||||||
SCDebounceWidget(
|
height: 48.w,
|
||||||
child: Column(
|
),
|
||||||
children: [
|
SizedBox(height: 8.w),
|
||||||
Image.asset(
|
text(
|
||||||
"sc_images/room/sc_icon_min_room.png",
|
SCAppLocalizations.of(context)!.exit,
|
||||||
width: 48.w,
|
fontSize: 15.sp,
|
||||||
height: 48.w,
|
textColor: Colors.white,
|
||||||
),
|
fontWeight: FontWeight.w500,
|
||||||
SizedBox(height: 8.w),
|
),
|
||||||
text(
|
],
|
||||||
SCAppLocalizations.of(context)!.mInimize,
|
),
|
||||||
fontSize: 15.sp,
|
onTap: () {
|
||||||
textColor: Colors.white,
|
final rtcProvider = context.read<RtcProvider>();
|
||||||
fontWeight: FontWeight.w500,
|
SCNavigatorUtils.goBack(context);
|
||||||
),
|
rtcProvider.exitCurrentVoiceRoomSession(false);
|
||||||
],
|
},
|
||||||
),
|
),
|
||||||
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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,66 +1,28 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:yumi/modules/room/voice_room_route.dart';
|
|
||||||
import 'package:yumi/services/music/room_music_manager.dart';
|
import 'package:yumi/services/music/room_music_manager.dart';
|
||||||
|
|
||||||
class RoomMusicFloatingEntry extends StatefulWidget {
|
class RoomMusicFloatingEntry extends StatelessWidget {
|
||||||
const RoomMusicFloatingEntry({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<RoomMusicManager, _FloatingMusicSnapshot>(
|
return Selector<RoomMusicManager, bool>(
|
||||||
selector:
|
selector: (_, manager) => manager.current != null,
|
||||||
(_, manager) => _FloatingMusicSnapshot(
|
builder: (context, visible, child) {
|
||||||
visible: manager.current != null,
|
if (!visible) {
|
||||||
playing: manager.isPlaying,
|
|
||||||
),
|
|
||||||
builder: (context, snapshot, child) {
|
|
||||||
if (!snapshot.visible) {
|
|
||||||
_controller.stop();
|
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
if (snapshot.playing && !_controller.isAnimating) {
|
return GestureDetector(
|
||||||
_controller.repeat();
|
behavior: HitTestBehavior.opaque,
|
||||||
} else if (!snapshot.playing && _controller.isAnimating) {
|
onTap: context.read<RoomMusicManager>().toggleRoomPlayerVisible,
|
||||||
_controller.stop();
|
child: SizedBox(
|
||||||
}
|
width: 44.w,
|
||||||
return PositionedDirectional(
|
height: 44.w,
|
||||||
end: 8.w,
|
child: Image.asset(
|
||||||
top: 292.w,
|
"sc_images/room/sc_music_material_room_side.png",
|
||||||
child: GestureDetector(
|
fit: BoxFit.contain,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -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(
|
GestureDetector(
|
||||||
onPressed: () => manager.stop(clearCurrent: true),
|
behavior: HitTestBehavior.opaque,
|
||||||
icon: const Icon(Icons.close, color: Colors.white),
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Image.asset("sc_images/room/${item.icon}", width: 45.w, height: 45.w),
|
_buildMenuIcon(item),
|
||||||
SizedBox(height: 10.w),
|
SizedBox(height: 10.w),
|
||||||
text(
|
text(
|
||||||
item.title,
|
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 {
|
class RoomMenu {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_screenutil/flutter_screenutil.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_banner_view.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/room_game_bottom_sheet.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 {
|
class RoomPlayWidget extends StatefulWidget {
|
||||||
const RoomPlayWidget({super.key});
|
const RoomPlayWidget({super.key});
|
||||||
@ -16,6 +17,11 @@ class _RoomPlayWidgetState extends State<RoomPlayWidget> {
|
|||||||
return SizedBox.expand(
|
return SizedBox.expand(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
PositionedDirectional(
|
||||||
|
end: 15.w,
|
||||||
|
bottom: 150.w,
|
||||||
|
child: const RoomMusicFloatingEntry(),
|
||||||
|
),
|
||||||
PositionedDirectional(
|
PositionedDirectional(
|
||||||
end: 15.w,
|
end: 15.w,
|
||||||
bottom: 100.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` 入口并跳转音乐页;语音房页面已挂载音乐悬浮入口,离开房间时会主动停止当前房间音乐,避免混音残留到房外页面。
|
||||||
- 已按最新反馈调整房间菜单位置:`Music` 不再单独占用菜单第一行,而是并入常规菜单网格,紧跟在 `Background` 图标之后;背景入口功能开关保持打开状态。
|
- 已按最新反馈调整房间菜单位置:`Music` 不再单独占用菜单第一行,而是并入常规菜单网格,紧跟在 `Background` 图标之后;背景入口功能开关保持打开状态。
|
||||||
- 已修正本地音乐扫描器的兼容性风险:扫描 MP3 时不再直接读取 `SongModel.fileExtension/displayName/title` 等可能因 Android MediaStore 返回空值而触发异常的 getter,改为从插件原始 map 安全取值,并对文件存在性检查和扫描失败做兜底。
|
- 已修正本地音乐扫描器的兼容性风险:扫描 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 文案承担。
|
- 已补齐 `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 和声网都完成后才进入完整房间”的体验存在差异。
|
- 已按竞品加载思路梳理当前语言房进房链路:当前业务 `entryRoom` 成功后会先进入房间页,IM 加群、声网入会、麦位与在线用户刷新在房间页展示后继续进行;这与“IM 和声网都完成后才进入完整房间”的体验存在差异。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user