💄 Mixed import button
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:groovybox/providers/db_provider.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import '../providers/db_provider.dart';
|
|
||||||
import 'db.dart';
|
import 'db.dart';
|
||||||
|
|
||||||
part 'playlist_repository.g.dart';
|
part 'playlist_repository.g.dart';
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter_media_metadata/flutter_media_metadata.dart';
|
import 'package:flutter_media_metadata/flutter_media_metadata.dart';
|
||||||
|
import 'package:groovybox/providers/db_provider.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import '../providers/db_provider.dart';
|
|
||||||
import 'db.dart';
|
import 'db.dart';
|
||||||
|
|
||||||
part 'track_repository.g.dart';
|
part 'track_repository.g.dart';
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
import 'package:groovybox/logic/audio_handler.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
import '../logic/audio_handler.dart';
|
|
||||||
|
|
||||||
part 'audio_provider.g.dart';
|
part 'audio_provider.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import 'package:groovybox/data/db.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import '../data/db.dart';
|
|
||||||
|
|
||||||
part 'db_provider.g.dart';
|
part 'db_provider.g.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:groovybox/data/db.dart';
|
||||||
|
import 'package:groovybox/data/playlist_repository.dart';
|
||||||
|
import 'package:groovybox/providers/audio_provider.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:media_kit/media_kit.dart' hide Track;
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import '../../data/playlist_repository.dart';
|
|
||||||
import '../../data/db.dart';
|
|
||||||
import '../../providers/audio_provider.dart';
|
|
||||||
|
|
||||||
class AlbumDetailScreen extends HookConsumerWidget {
|
class AlbumDetailScreen extends HookConsumerWidget {
|
||||||
final AlbumData album;
|
final AlbumData album;
|
||||||
|
|||||||
@@ -3,19 +3,40 @@ import 'package:file_picker/file_picker.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:groovybox/data/db.dart';
|
||||||
|
import 'package:groovybox/data/playlist_repository.dart';
|
||||||
|
import 'package:groovybox/data/track_repository.dart';
|
||||||
|
import 'package:groovybox/logic/lyrics_parser.dart';
|
||||||
|
import 'package:groovybox/providers/audio_provider.dart';
|
||||||
|
import 'package:groovybox/ui/tabs/albums_tab.dart';
|
||||||
|
import 'package:groovybox/ui/tabs/playlists_tab.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
import '../../data/track_repository.dart';
|
|
||||||
import '../../providers/audio_provider.dart';
|
|
||||||
import '../../data/playlist_repository.dart';
|
|
||||||
import '../../data/db.dart';
|
|
||||||
import '../../logic/lyrics_parser.dart';
|
|
||||||
import '../tabs/albums_tab.dart';
|
|
||||||
import '../tabs/playlists_tab.dart';
|
|
||||||
|
|
||||||
class LibraryScreen extends HookConsumerWidget {
|
class LibraryScreen extends HookConsumerWidget {
|
||||||
const LibraryScreen({super.key});
|
const LibraryScreen({super.key});
|
||||||
|
|
||||||
|
static const List<String> audioExtensions = [
|
||||||
|
'mp3',
|
||||||
|
'm4a',
|
||||||
|
'wav',
|
||||||
|
'flac',
|
||||||
|
'aac',
|
||||||
|
'ogg',
|
||||||
|
'wma',
|
||||||
|
'm4p',
|
||||||
|
'aiff',
|
||||||
|
'au',
|
||||||
|
'dss',
|
||||||
|
];
|
||||||
|
|
||||||
|
static const List<String> lyricsExtensions = ['lrc', 'srt', 'txt'];
|
||||||
|
|
||||||
|
static const List<String> allAllowedExtensions = [
|
||||||
|
...audioExtensions,
|
||||||
|
...lyricsExtensions,
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// We can define a stream provider locally or in repository file.
|
// We can define a stream provider locally or in repository file.
|
||||||
@@ -81,6 +102,7 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
: AppBar(
|
: AppBar(
|
||||||
|
centerTitle: true,
|
||||||
title: const Text('Library'),
|
title: const Text('Library'),
|
||||||
bottom: const TabBar(
|
bottom: const TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
@@ -92,29 +114,52 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.add_circle_outline),
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
tooltip: 'Add Tracks',
|
tooltip: 'Import Files',
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.audio,
|
type: FileType.custom,
|
||||||
|
allowedExtensions: allAllowedExtensions,
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
);
|
);
|
||||||
if (result != null) {
|
if (result != null && result.files.isNotEmpty) {
|
||||||
// Collect paths
|
|
||||||
final paths = result.files
|
final paths = result.files
|
||||||
.map((f) => f.path)
|
.map((f) => f.path)
|
||||||
.whereType<String>()
|
.whereType<String>()
|
||||||
.toList();
|
.toList();
|
||||||
if (paths.isNotEmpty) {
|
if (paths.isNotEmpty) {
|
||||||
await repo.importFiles(paths);
|
// Separate audio and lyrics files
|
||||||
|
final audioPaths = paths.where((path) {
|
||||||
|
final ext = p
|
||||||
|
.extension(path)
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceFirst('.', '');
|
||||||
|
return audioExtensions.contains(ext);
|
||||||
|
}).toList();
|
||||||
|
final lyricsPaths = paths.where((path) {
|
||||||
|
final ext = p
|
||||||
|
.extension(path)
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceFirst('.', '');
|
||||||
|
return lyricsExtensions.contains(ext);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Import tracks if any
|
||||||
|
if (audioPaths.isNotEmpty) {
|
||||||
|
await repo.importFiles(audioPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import lyrics if any
|
||||||
|
if (lyricsPaths.isNotEmpty) {
|
||||||
|
await _batchImportLyricsFromPaths(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
lyricsPaths,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.lyrics_outlined),
|
|
||||||
tooltip: 'Batch Import Lyrics',
|
|
||||||
onPressed: () => _batchImportLyrics(context, ref),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -609,14 +654,12 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _batchImportLyrics(BuildContext context, WidgetRef ref) async {
|
Future<void> _batchImportLyricsFromPaths(
|
||||||
final result = await FilePicker.platform.pickFiles(
|
BuildContext context,
|
||||||
type: FileType.custom,
|
WidgetRef ref,
|
||||||
allowedExtensions: ['lrc', 'srt', 'txt'],
|
List<String> lyricsPaths,
|
||||||
allowMultiple: true,
|
) async {
|
||||||
);
|
if (lyricsPaths.isEmpty) return;
|
||||||
|
|
||||||
if (result == null || result.files.isEmpty) return;
|
|
||||||
|
|
||||||
final repo = ref.read(trackRepositoryProvider.notifier);
|
final repo = ref.read(trackRepositoryProvider.notifier);
|
||||||
final tracks = await repo.getAllTracks();
|
final tracks = await repo.getAllTracks();
|
||||||
@@ -624,12 +667,10 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
int matched = 0;
|
int matched = 0;
|
||||||
int notMatched = 0;
|
int notMatched = 0;
|
||||||
|
|
||||||
for (final pickedFile in result.files) {
|
for (final path in lyricsPaths) {
|
||||||
if (pickedFile.path == null) continue;
|
final file = File(path);
|
||||||
|
|
||||||
final file = File(pickedFile.path!);
|
|
||||||
final content = await file.readAsString();
|
final content = await file.readAsString();
|
||||||
final filename = pickedFile.name;
|
final filename = p.basename(path);
|
||||||
|
|
||||||
// Get basename without extension for matching
|
// Get basename without extension for matching
|
||||||
final baseName = filename
|
final baseName = filename
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:groovybox/data/db.dart' as db;
|
||||||
|
import 'package:groovybox/logic/lyrics_parser.dart';
|
||||||
|
import 'package:groovybox/logic/metadata_service.dart';
|
||||||
|
import 'package:groovybox/providers/audio_provider.dart';
|
||||||
|
import 'package:groovybox/providers/db_provider.dart';
|
||||||
|
import 'package:groovybox/ui/widgets/mini_player.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
|
|
||||||
import '../../providers/audio_provider.dart';
|
|
||||||
import '../../providers/db_provider.dart';
|
|
||||||
import '../../logic/metadata_service.dart';
|
|
||||||
import '../../logic/lyrics_parser.dart';
|
|
||||||
import '../../data/db.dart' as db;
|
|
||||||
import '../widgets/mini_player.dart';
|
|
||||||
|
|
||||||
class PlayerScreen extends HookConsumerWidget {
|
class PlayerScreen extends HookConsumerWidget {
|
||||||
const PlayerScreen({super.key});
|
const PlayerScreen({super.key});
|
||||||
|
|
||||||
|
|||||||
128
lib/ui/screens/playlist_detail_screen.dart
Normal file
128
lib/ui/screens/playlist_detail_screen.dart
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:groovybox/data/db.dart';
|
||||||
|
import 'package:groovybox/data/playlist_repository.dart';
|
||||||
|
import 'package:groovybox/providers/audio_provider.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart' hide Track, Playlist;
|
||||||
|
|
||||||
|
class PlaylistDetailScreen extends HookConsumerWidget {
|
||||||
|
final Playlist playlist;
|
||||||
|
|
||||||
|
const PlaylistDetailScreen({super.key, required this.playlist});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final repo = ref.watch(playlistRepositoryProvider.notifier);
|
||||||
|
final tracksAsync = repo.watchPlaylistTracks(playlist.id);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
expandedHeight: 200,
|
||||||
|
pinned: true,
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
title: Text(playlist.name),
|
||||||
|
background: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.purple.withOpacity(0.8),
|
||||||
|
Colors.blue.withOpacity(0.6),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.queue_music,
|
||||||
|
size: 80,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StreamBuilder<List<Track>>(
|
||||||
|
stream: tracksAsync,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return const SliverFillRemaining(
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final tracks = snapshot.data!;
|
||||||
|
if (tracks.isEmpty) {
|
||||||
|
return const SliverFillRemaining(
|
||||||
|
child: Center(child: Text('No tracks in this playlist')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
_playPlaylist(ref, tracks);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.play_arrow),
|
||||||
|
label: const Text('Play All'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildTrackTile(ref, tracks, index),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _buildTrackTile(ref, tracks, index);
|
||||||
|
}, childCount: tracks.length),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTrackTile(WidgetRef ref, List<Track> tracks, int index) {
|
||||||
|
final track = tracks[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: Text(
|
||||||
|
'${index + 1}',
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
||||||
|
),
|
||||||
|
title: Text(track.title),
|
||||||
|
subtitle: Text(track.artist ?? 'Unknown Artist'),
|
||||||
|
trailing: Text(_formatDuration(track.duration)),
|
||||||
|
onTap: () {
|
||||||
|
_playPlaylist(ref, tracks, initialIndex: index);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _playPlaylist(
|
||||||
|
WidgetRef ref,
|
||||||
|
List<Track> tracks, {
|
||||||
|
int initialIndex = 0,
|
||||||
|
}) {
|
||||||
|
final audioHandler = ref.read(audioHandlerProvider);
|
||||||
|
final medias = tracks.map((t) => Media(t.path)).toList();
|
||||||
|
audioHandler.openPlaylist(medias, initialIndex: initialIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDuration(int? durationMs) {
|
||||||
|
if (durationMs == null) return '--:--';
|
||||||
|
final d = Duration(milliseconds: durationMs);
|
||||||
|
final minutes = d.inMinutes;
|
||||||
|
final seconds = d.inSeconds % 60;
|
||||||
|
return '$minutes:${seconds.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:animations/animations.dart';
|
import 'package:animations/animations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:groovybox/data/playlist_repository.dart';
|
||||||
|
import 'package:groovybox/ui/screens/album_detail_screen.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import '../../data/playlist_repository.dart';
|
|
||||||
import '../screens/album_detail_screen.dart';
|
|
||||||
|
|
||||||
class AlbumsTab extends HookConsumerWidget {
|
class AlbumsTab extends HookConsumerWidget {
|
||||||
const AlbumsTab({super.key});
|
const AlbumsTab({super.key});
|
||||||
@@ -15,8 +15,9 @@ class AlbumsTab extends HookConsumerWidget {
|
|||||||
return StreamBuilder<List<AlbumData>>(
|
return StreamBuilder<List<AlbumData>>(
|
||||||
stream: repo.watchAllAlbums(),
|
stream: repo.watchAllAlbums(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData)
|
if (!snapshot.hasData) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
final albums = snapshot.data!;
|
final albums = snapshot.data!;
|
||||||
|
|
||||||
if (albums.isEmpty) {
|
if (albums.isEmpty) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:groovybox/data/db.dart';
|
||||||
|
import 'package:groovybox/data/playlist_repository.dart';
|
||||||
|
import 'package:groovybox/ui/screens/playlist_detail_screen.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import '../../data/db.dart';
|
|
||||||
import '../../data/playlist_repository.dart';
|
|
||||||
|
|
||||||
class PlaylistsTab extends HookConsumerWidget {
|
class PlaylistsTab extends HookConsumerWidget {
|
||||||
const PlaylistsTab({super.key});
|
const PlaylistsTab({super.key});
|
||||||
@@ -44,8 +45,9 @@ class PlaylistsTab extends HookConsumerWidget {
|
|||||||
body: StreamBuilder<List<Playlist>>(
|
body: StreamBuilder<List<Playlist>>(
|
||||||
stream: repo.watchAllPlaylists(),
|
stream: repo.watchAllPlaylists(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData)
|
if (!snapshot.hasData) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
final playlists = snapshot.data!;
|
final playlists = snapshot.data!;
|
||||||
|
|
||||||
if (playlists.isEmpty) {
|
if (playlists.isEmpty) {
|
||||||
@@ -67,9 +69,12 @@ class PlaylistsTab extends HookConsumerWidget {
|
|||||||
onPressed: () => repo.deletePlaylist(playlist.id),
|
onPressed: () => repo.deletePlaylist(playlist.id),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Navigate to playlist details
|
Navigator.push(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
context,
|
||||||
SnackBar(content: Text('Open ${playlist.name}')),
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
PlaylistDetailScreen(playlist: playlist),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:groovybox/logic/metadata_service.dart';
|
||||||
|
import 'package:groovybox/providers/audio_provider.dart';
|
||||||
|
import 'package:groovybox/ui/screens/player_screen.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import '../../providers/audio_provider.dart';
|
|
||||||
import '../../logic/metadata_service.dart';
|
|
||||||
import '../screens/player_screen.dart';
|
|
||||||
|
|
||||||
class MiniPlayer extends HookConsumerWidget {
|
class MiniPlayer extends HookConsumerWidget {
|
||||||
final bool enableTapToOpen;
|
final bool enableTapToOpen;
|
||||||
|
|||||||
Reference in New Issue
Block a user