Add audio player functionality to file explorer and integrate just_audio package
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:b0esche_cloud/widgets/audio_player_bar.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'dart:js_interop';
|
import 'dart:js_interop';
|
||||||
@@ -35,7 +36,8 @@ class FileExplorer extends StatefulWidget {
|
|||||||
State<FileExplorer> createState() => _FileExplorerState();
|
State<FileExplorer> createState() => _FileExplorerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FileExplorerState extends State<FileExplorer> {
|
class _FileExplorerState extends State<FileExplorer>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
String _getFileTypeLabel(FileItem file) {
|
String _getFileTypeLabel(FileItem file) {
|
||||||
if (file.type == FileType.folder) return 'Folder';
|
if (file.type == FileType.folder) return 'Folder';
|
||||||
final name = file.name.toLowerCase();
|
final name = file.name.toLowerCase();
|
||||||
@@ -111,6 +113,11 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? _selectedFilePath;
|
String? _selectedFilePath;
|
||||||
|
String? _audioFileName;
|
||||||
|
String? _audioFileUrl;
|
||||||
|
bool _showAudioBar = false;
|
||||||
|
late AnimationController _audioBarController;
|
||||||
|
late Animation<Offset> _audioBarOffset;
|
||||||
bool _isSearching = false;
|
bool _isSearching = false;
|
||||||
bool _showField = false;
|
bool _showField = false;
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
@@ -138,12 +145,29 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_audioBarController.dispose();
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_audioBarController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
);
|
||||||
|
_audioBarOffset =
|
||||||
|
Tween<Offset>(begin: const Offset(0, -1), end: Offset.zero).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _audioBarController,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
context.read<FileBrowserBloc>().add(
|
||||||
|
LoadDirectory(orgId: widget.orgId, path: '/'),
|
||||||
|
);
|
||||||
|
context.read<PermissionBloc>().add(LoadPermissions(widget.orgId));
|
||||||
super.initState();
|
super.initState();
|
||||||
context.read<FileBrowserBloc>().add(
|
context.read<FileBrowserBloc>().add(
|
||||||
LoadDirectory(orgId: widget.orgId, path: '/'),
|
LoadDirectory(orgId: widget.orgId, path: '/'),
|
||||||
@@ -863,178 +887,214 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
|
|
||||||
Widget _buildTitle() {
|
Widget _buildTitle() {
|
||||||
const double titleWidth = 72.0;
|
const double titleWidth = 72.0;
|
||||||
return SizedBox(
|
return Column(
|
||||||
width: double.infinity,
|
mainAxisSize: MainAxisSize.min,
|
||||||
height: 50,
|
children: [
|
||||||
child: Stack(
|
SizedBox(
|
||||||
alignment: Alignment.centerLeft,
|
width: double.infinity,
|
||||||
children: [
|
height: 50,
|
||||||
const Positioned(
|
child: Stack(
|
||||||
left: 0,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
children: [
|
||||||
'/Drive',
|
const Positioned(
|
||||||
style: TextStyle(
|
left: 0,
|
||||||
fontSize: 24,
|
child: Text(
|
||||||
color: AppTheme.primaryText,
|
'/Drive',
|
||||||
fontWeight: FontWeight.bold,
|
style: TextStyle(
|
||||||
),
|
fontSize: 24,
|
||||||
),
|
color: AppTheme.primaryText,
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
AnimatedPositioned(
|
),
|
||||||
left: _isSearching ? titleWidth + 250.0 : titleWidth,
|
|
||||||
duration: const Duration(milliseconds: 250),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 2),
|
|
||||||
child: IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
_isSearching ? Icons.close : Icons.search,
|
|
||||||
color: AppTheme.accentColor,
|
|
||||||
),
|
),
|
||||||
splashColor: Colors.transparent,
|
|
||||||
highlightColor: Colors.transparent,
|
|
||||||
onPressed: () {
|
|
||||||
if (_isSearching) {
|
|
||||||
setState(() {
|
|
||||||
_showField = false;
|
|
||||||
_isSearching = false;
|
|
||||||
_searchController.clear();
|
|
||||||
_searchQuery = '';
|
|
||||||
context.read<FileBrowserBloc>().add(ApplyFilter(''));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_isSearching = true;
|
|
||||||
});
|
|
||||||
Future.delayed(const Duration(milliseconds: 150), () {
|
|
||||||
setState(() {
|
|
||||||
_showField = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
AnimatedPositioned(
|
||||||
),
|
left: _isSearching ? titleWidth + 250.0 : titleWidth,
|
||||||
Positioned(
|
duration: const Duration(milliseconds: 250),
|
||||||
right: 0,
|
curve: Curves.easeInOut,
|
||||||
top: 0,
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.only(top: 2),
|
||||||
padding: const EdgeInsets.only(top: 2),
|
child: IconButton(
|
||||||
child: BlocBuilder<FileBrowserBloc, FileBrowserState>(
|
icon: Icon(
|
||||||
builder: (context, state) {
|
_isSearching ? Icons.close : Icons.search,
|
||||||
String currentSort = 'name';
|
color: AppTheme.accentColor,
|
||||||
bool isAscending = true;
|
),
|
||||||
if (state is DirectoryLoaded) {
|
splashColor: Colors.transparent,
|
||||||
currentSort = state.sortBy;
|
highlightColor: Colors.transparent,
|
||||||
isAscending = state.isAscending;
|
onPressed: () {
|
||||||
}
|
if (_isSearching) {
|
||||||
return Row(
|
setState(() {
|
||||||
children: [
|
_showField = false;
|
||||||
PopupMenuButton<String>(
|
_isSearching = false;
|
||||||
color: AppTheme.accentColor.withAlpha(220),
|
_searchController.clear();
|
||||||
position: PopupMenuPosition.under,
|
_searchQuery = '';
|
||||||
offset: const Offset(48, 8),
|
context.read<FileBrowserBloc>().add(ApplyFilter(''));
|
||||||
itemBuilder: (BuildContext context) => const [
|
});
|
||||||
PopupMenuItem(value: 'name', child: Text('Name')),
|
} else {
|
||||||
PopupMenuItem(value: 'date', child: Text('Date')),
|
setState(() {
|
||||||
PopupMenuItem(value: 'size', child: Text('Size')),
|
_isSearching = true;
|
||||||
PopupMenuItem(value: 'type', child: Text('Type')),
|
});
|
||||||
|
Future.delayed(const Duration(milliseconds: 150), () {
|
||||||
|
setState(() {
|
||||||
|
_showField = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 2),
|
||||||
|
child: BlocBuilder<FileBrowserBloc, FileBrowserState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
String currentSort = 'name';
|
||||||
|
bool isAscending = true;
|
||||||
|
if (state is DirectoryLoaded) {
|
||||||
|
currentSort = state.sortBy;
|
||||||
|
isAscending = state.isAscending;
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
PopupMenuButton<String>(
|
||||||
|
color: AppTheme.accentColor.withAlpha(220),
|
||||||
|
position: PopupMenuPosition.under,
|
||||||
|
offset: const Offset(48, 8),
|
||||||
|
itemBuilder: (BuildContext context) => const [
|
||||||
|
PopupMenuItem(value: 'name', child: Text('Name')),
|
||||||
|
PopupMenuItem(value: 'date', child: Text('Date')),
|
||||||
|
PopupMenuItem(value: 'size', child: Text('Size')),
|
||||||
|
PopupMenuItem(value: 'type', child: Text('Type')),
|
||||||
|
],
|
||||||
|
onSelected: (value) {
|
||||||
|
context.read<FileBrowserBloc>().add(
|
||||||
|
ApplySort(value, isAscending: isAscending),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_drop_down,
|
||||||
|
color: AppTheme.primaryText,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
currentSort == 'name'
|
||||||
|
? 'Name'
|
||||||
|
: currentSort == 'date'
|
||||||
|
? 'Date'
|
||||||
|
: currentSort == 'size'
|
||||||
|
? 'Size'
|
||||||
|
: 'Type',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppTheme.primaryText,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isAscending
|
||||||
|
? Icons.arrow_upward
|
||||||
|
: Icons.arrow_downward,
|
||||||
|
color: AppTheme.accentColor,
|
||||||
|
),
|
||||||
|
splashColor: Colors.transparent,
|
||||||
|
highlightColor: Colors.transparent,
|
||||||
|
onPressed: () {
|
||||||
|
context.read<FileBrowserBloc>().add(
|
||||||
|
ApplySort(
|
||||||
|
currentSort,
|
||||||
|
isAscending: !isAscending,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
onSelected: (value) {
|
);
|
||||||
context.read<FileBrowserBloc>().add(
|
},
|
||||||
ApplySort(value, isAscending: isAscending),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_drop_down,
|
|
||||||
color: AppTheme.primaryText,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
currentSort == 'name'
|
|
||||||
? 'Name'
|
|
||||||
: currentSort == 'date'
|
|
||||||
? 'Date'
|
|
||||||
: currentSort == 'size'
|
|
||||||
? 'Size'
|
|
||||||
: 'Type',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppTheme.primaryText,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
isAscending
|
|
||||||
? Icons.arrow_upward
|
|
||||||
: Icons.arrow_downward,
|
|
||||||
color: AppTheme.accentColor,
|
|
||||||
),
|
|
||||||
splashColor: Colors.transparent,
|
|
||||||
highlightColor: Colors.transparent,
|
|
||||||
onPressed: () {
|
|
||||||
context.read<FileBrowserBloc>().add(
|
|
||||||
ApplySort(currentSort, isAscending: !isAscending),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
if (_showField)
|
||||||
),
|
AnimatedPositioned(
|
||||||
if (_showField)
|
left: titleWidth,
|
||||||
AnimatedPositioned(
|
duration: const Duration(milliseconds: 300),
|
||||||
left: titleWidth,
|
curve: Curves.easeInOut,
|
||||||
duration: const Duration(milliseconds: 300),
|
child: SizedBox(
|
||||||
curve: Curves.easeInOut,
|
width: 250,
|
||||||
child: SizedBox(
|
child: TextField(
|
||||||
width: 250,
|
controller: _searchController,
|
||||||
child: TextField(
|
autofocus: true,
|
||||||
controller: _searchController,
|
style: const TextStyle(color: AppTheme.primaryText),
|
||||||
autofocus: true,
|
cursorColor: AppTheme.accentColor,
|
||||||
style: const TextStyle(color: AppTheme.primaryText),
|
decoration: InputDecoration(
|
||||||
cursorColor: AppTheme.accentColor,
|
hintText: 'Search Files...',
|
||||||
decoration: InputDecoration(
|
hintStyle: const TextStyle(
|
||||||
hintText: 'Search Files...',
|
color: AppTheme.secondaryText,
|
||||||
hintStyle: const TextStyle(color: AppTheme.secondaryText),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
vertical: 2,
|
vertical: 2,
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppTheme.secondaryText.withValues(alpha: 0.5),
|
color: AppTheme.secondaryText.withValues(
|
||||||
|
alpha: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(24)),
|
||||||
|
borderSide: BorderSide(color: AppTheme.accentColor),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
onChanged: (value) {
|
||||||
focusedBorder: const OutlineInputBorder(
|
_searchQuery = value;
|
||||||
borderRadius: BorderRadius.all(Radius.circular(24)),
|
context.read<FileBrowserBloc>().add(
|
||||||
borderSide: BorderSide(color: AppTheme.accentColor),
|
ApplyFilter(_searchQuery),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
|
||||||
_searchQuery = value;
|
|
||||||
context.read<FileBrowserBloc>().add(
|
|
||||||
ApplyFilter(_searchQuery),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
// Animated audio bar
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _audioBarController,
|
||||||
|
builder: (context, child) {
|
||||||
|
return (_showAudioBar &&
|
||||||
|
_audioFileName != null &&
|
||||||
|
_audioFileUrl != null)
|
||||||
|
? SlideTransition(
|
||||||
|
position: _audioBarOffset,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: AudioPlayerBar(
|
||||||
|
fileName: _audioFileName!,
|
||||||
|
fileUrl: _audioFileUrl!,
|
||||||
|
onClose: () {
|
||||||
|
_audioBarController.reverse();
|
||||||
|
setState(() => _showAudioBar = false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1064,6 +1124,20 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
(ext) => file.name.toLowerCase().endsWith(ext),
|
(ext) => file.name.toLowerCase().endsWith(ext),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final ext = p.extension(file.name).toLowerCase();
|
||||||
|
const audioExts = [
|
||||||
|
'.mp3',
|
||||||
|
'.wav',
|
||||||
|
'.flac',
|
||||||
|
'.ogg',
|
||||||
|
'.aac',
|
||||||
|
'.m4a',
|
||||||
|
'.opus',
|
||||||
|
'.wma',
|
||||||
|
'.alac',
|
||||||
|
'.aiff',
|
||||||
|
'.amr',
|
||||||
|
];
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
onEnter: (_) => setState(() => _hovered[file.path] = true),
|
onEnter: (_) => setState(() => _hovered[file.path] = true),
|
||||||
onExit: (_) => setState(() => _hovered[file.path] = false),
|
onExit: (_) => setState(() => _hovered[file.path] = false),
|
||||||
@@ -1075,7 +1149,6 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
if (file.type == FileType.folder) {
|
if (file.type == FileType.folder) {
|
||||||
context.read<FileBrowserBloc>().add(NavigateToFolder(file.path));
|
context.read<FileBrowserBloc>().add(NavigateToFolder(file.path));
|
||||||
} else if (isVideo) {
|
} else if (isVideo) {
|
||||||
// Open video files in video viewer - use viewer session for authenticated URL
|
|
||||||
if (file.id == null || file.id!.isEmpty) {
|
if (file.id == null || file.id!.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Error: File ID is missing')),
|
const SnackBar(content: Text('Error: File ID is missing')),
|
||||||
@@ -1095,6 +1168,25 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (audioExts.contains(ext)) {
|
||||||
|
if (file.id == null || file.id!.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Error: File ID is missing')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final fileService = getIt<FileService>();
|
||||||
|
final url = await fileService.getFileUrl(
|
||||||
|
orgId: widget.orgId,
|
||||||
|
fileId: file.id!,
|
||||||
|
fileName: file.name,
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_audioFileName = file.name;
|
||||||
|
_audioFileUrl = url;
|
||||||
|
_showAudioBar = true;
|
||||||
|
});
|
||||||
|
_audioBarController.forward();
|
||||||
} else {
|
} else {
|
||||||
if (file.id == null || file.id!.isEmpty) {
|
if (file.id == null || file.id!.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|||||||
139
b0esche_cloud/lib/widgets/audio_player_bar.dart
Normal file
139
b0esche_cloud/lib/widgets/audio_player_bar.dart
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:just_audio/just_audio.dart';
|
||||||
|
|
||||||
|
class AudioPlayerBar extends StatefulWidget {
|
||||||
|
final String fileName;
|
||||||
|
final String fileUrl;
|
||||||
|
final VoidCallback? onClose;
|
||||||
|
|
||||||
|
const AudioPlayerBar({
|
||||||
|
super.key,
|
||||||
|
required this.fileName,
|
||||||
|
required this.fileUrl,
|
||||||
|
this.onClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AudioPlayerBar> createState() => _AudioPlayerBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AudioPlayerBarState extends State<AudioPlayerBar> {
|
||||||
|
late AudioPlayer _audioPlayer;
|
||||||
|
Duration _duration = Duration.zero;
|
||||||
|
Duration _position = Duration.zero;
|
||||||
|
bool _isPlaying = false;
|
||||||
|
bool _isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_audioPlayer = AudioPlayer();
|
||||||
|
_initAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initAudio() async {
|
||||||
|
try {
|
||||||
|
await _audioPlayer.setUrl(widget.fileUrl);
|
||||||
|
setState(() {
|
||||||
|
_duration = _audioPlayer.duration ?? Duration.zero;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
_audioPlayer.positionStream.listen((pos) {
|
||||||
|
setState(() {
|
||||||
|
_position = pos;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
_audioPlayer.playerStateStream.listen((state) {
|
||||||
|
setState(() {
|
||||||
|
_isPlaying = state.playing;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
// Optionally show error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_audioPlayer.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDuration(Duration d) {
|
||||||
|
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||||
|
final minutes = twoDigits(d.inMinutes.remainder(60));
|
||||||
|
final seconds = twoDigits(d.inSeconds.remainder(60));
|
||||||
|
return '$minutes:$seconds';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
elevation: 8,
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
height: 64,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
|
||||||
|
onPressed: _isLoading
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (_isPlaying) {
|
||||||
|
_audioPlayer.pause();
|
||||||
|
} else {
|
||||||
|
_audioPlayer.play();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.fileName,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Slider(
|
||||||
|
min: 0,
|
||||||
|
max: _duration.inMilliseconds.toDouble(),
|
||||||
|
value: _position.inMilliseconds
|
||||||
|
.clamp(0, _duration.inMilliseconds)
|
||||||
|
.toDouble(),
|
||||||
|
onChanged: _isLoading
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
|
_audioPlayer.seek(
|
||||||
|
Duration(milliseconds: value.toInt()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Text(
|
||||||
|
'${_formatDuration(_position)} / ${_formatDuration(_duration)}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.onClose != null)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: widget.onClose,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,14 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import audio_session
|
||||||
import connectivity_plus
|
import connectivity_plus
|
||||||
import desktop_drop
|
import desktop_drop
|
||||||
import device_info_plus
|
import device_info_plus
|
||||||
import file_picker
|
import file_picker
|
||||||
import flutter_secure_storage_darwin
|
import flutter_secure_storage_darwin
|
||||||
import irondash_engine_context
|
import irondash_engine_context
|
||||||
|
import just_audio
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import super_native_extensions
|
import super_native_extensions
|
||||||
@@ -19,12 +21,14 @@ import url_launcher_macos
|
|||||||
import video_player_avfoundation
|
import video_player_avfoundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||||
IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin"))
|
IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin"))
|
||||||
|
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin"))
|
SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin"))
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.0"
|
||||||
|
audio_session:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audio_session
|
||||||
|
sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
bloc:
|
bloc:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -696,6 +704,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.8.0"
|
version: "6.8.0"
|
||||||
|
just_audio:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: just_audio
|
||||||
|
sha256: "9694e4734f515f2a052493d1d7e0d6de219ee0427c7c29492e246ff32a219908"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.5"
|
||||||
|
just_audio_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: just_audio_platform_interface
|
||||||
|
sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.6.0"
|
||||||
|
just_audio_web:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: just_audio_web
|
||||||
|
sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.16"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ dependencies:
|
|||||||
# Video Playback
|
# Video Playback
|
||||||
video_player: ^2.8.2
|
video_player: ^2.8.2
|
||||||
syncfusion_flutter_core: ^31.2.18
|
syncfusion_flutter_core: ^31.2.18
|
||||||
|
just_audio_web: ^0.4.16
|
||||||
|
just_audio: ^0.10.5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user