АААААА СУКО ПЛЕИР МУЗЫКО
This commit is contained in:
329
lib/screens/music_library_screen.dart
Normal file
329
lib/screens/music_library_screen.dart
Normal file
@@ -0,0 +1,329 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:io' as io;
|
||||
import 'dart:convert';
|
||||
import '../services/music_player_service.dart';
|
||||
import '../widgets/bottom_sheet_music_player.dart';
|
||||
|
||||
class MusicLibraryScreen extends StatefulWidget {
|
||||
const MusicLibraryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<MusicLibraryScreen> createState() => _MusicLibraryScreenState();
|
||||
}
|
||||
|
||||
class _MusicLibraryScreenState extends State<MusicLibraryScreen> {
|
||||
List<MusicTrack> _musicTracks = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadMusicTracks();
|
||||
}
|
||||
|
||||
Future<void> _loadMusicTracks() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final fileIdMap = prefs.getStringList('file_id_to_path_map') ?? [];
|
||||
final List<MusicTrack> tracks = [];
|
||||
|
||||
final musicMetadataJson = prefs.getString('music_metadata') ?? '{}';
|
||||
final Map<String, dynamic> musicMetadata = jsonDecode(musicMetadataJson);
|
||||
|
||||
for (final mapping in fileIdMap) {
|
||||
final parts = mapping.split(':');
|
||||
if (parts.length >= 2) {
|
||||
final fileId = parts[0];
|
||||
final filePath = parts.skip(1).join(':');
|
||||
final file = io.File(filePath);
|
||||
|
||||
if (await file.exists()) {
|
||||
final extension = filePath.split('.').last.toLowerCase();
|
||||
if ([
|
||||
'mp3',
|
||||
'wav',
|
||||
'flac',
|
||||
'm4a',
|
||||
'aac',
|
||||
'ogg',
|
||||
].contains(extension)) {
|
||||
final metadata = musicMetadata[fileId] as Map<String, dynamic>?;
|
||||
|
||||
if (metadata != null) {
|
||||
tracks.add(
|
||||
MusicTrack.fromJson({
|
||||
...metadata,
|
||||
'filePath': filePath,
|
||||
'fileId': int.tryParse(fileId),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
final fileName = filePath.split('/').last;
|
||||
final nameWithoutExt = fileName.substring(
|
||||
0,
|
||||
fileName.lastIndexOf('.'),
|
||||
);
|
||||
tracks.add(
|
||||
MusicTrack(
|
||||
id: fileId,
|
||||
title: nameWithoutExt,
|
||||
artist: 'Unknown Artist',
|
||||
filePath: filePath,
|
||||
fileId: int.tryParse(fileId),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracks.sort((a, b) => a.title.compareTo(b.title));
|
||||
|
||||
setState(() {
|
||||
_musicTracks = tracks;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
print('Error loading music tracks: $e');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _playTrack(MusicTrack track) async {
|
||||
final musicPlayer = MusicPlayerService();
|
||||
await musicPlayer.playTrack(track, playlist: _musicTracks);
|
||||
}
|
||||
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes < 1024) {
|
||||
return '$bytes B';
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
} else {
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDuration(int? milliseconds) {
|
||||
if (milliseconds == null) return '--:--';
|
||||
final duration = Duration(milliseconds: milliseconds);
|
||||
final minutes = duration.inMinutes;
|
||||
final seconds = duration.inSeconds.remainder(60);
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final musicPlayer = context.watch<MusicPlayerService>();
|
||||
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: BottomSheetMusicPlayer.isExpandedNotifier,
|
||||
builder: (context, isPlayerExpanded, child) {
|
||||
return PopScope(
|
||||
canPop: !isPlayerExpanded,
|
||||
onPopInvoked: (didPop) {
|
||||
if (!didPop && isPlayerExpanded) {
|
||||
BottomSheetMusicPlayer.isExpandedNotifier.value = false;
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: const Text('Музыка')),
|
||||
body: Stack(
|
||||
children: [
|
||||
_isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _musicTracks.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.music_off,
|
||||
size: 64,
|
||||
color: colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Нет музыки',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Скачайте музыку из чатов',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: musicPlayer.currentTrack != null ? 120 : 16,
|
||||
),
|
||||
itemCount: _musicTracks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final track = _musicTracks[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
color: colorScheme.primaryContainer,
|
||||
child: track.albumArtUrl != null
|
||||
? Image.network(
|
||||
track.albumArtUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
Icon(
|
||||
Icons.music_note,
|
||||
color: colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.music_note,
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
track.title,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
track.artist,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(
|
||||
0.7,
|
||||
),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (track.album != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
track.album!,
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: colorScheme.onSurface
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
if (track.duration != null) ...[
|
||||
Text(
|
||||
_formatDuration(track.duration),
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: colorScheme.onSurface
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'•',
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: colorScheme.onSurface
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
if (track.filePath != null) ...[
|
||||
FutureBuilder<io.FileStat>(
|
||||
future: io.File(
|
||||
track.filePath!,
|
||||
).stat(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text(
|
||||
_formatFileSize(
|
||||
snapshot.data!.size,
|
||||
),
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: IconButton(
|
||||
onPressed: () => _playTrack(track),
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
onTap: () => _playTrack(track),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (musicPlayer.currentTrack != null)
|
||||
const Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: BottomSheetMusicPlayer(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user