♻️ Better current track metadata
This commit is contained in:
@@ -1,7 +1,11 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:media_kit/media_kit.dart' as media_kit;
|
import 'package:media_kit/media_kit.dart' as media_kit;
|
||||||
import 'package:groovybox/data/db.dart';
|
import 'package:groovybox/data/db.dart';
|
||||||
|
import 'package:groovybox/logic/metadata_service.dart';
|
||||||
|
import 'package:groovybox/providers/audio_provider.dart';
|
||||||
import 'package:groovybox/providers/theme_provider.dart';
|
import 'package:groovybox/providers/theme_provider.dart';
|
||||||
import 'package:groovybox/providers/remote_provider.dart';
|
import 'package:groovybox/providers/remote_provider.dart';
|
||||||
import 'package:groovybox/providers/db_provider.dart';
|
import 'package:groovybox/providers/db_provider.dart';
|
||||||
@@ -47,11 +51,13 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
|
|||||||
_container = container;
|
_container = container;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update theme color based on current track's album art
|
// Update theme color based on current track's album art and set current metadata
|
||||||
void _updateThemeFromCurrentTrack(MediaItem mediaItem) async {
|
void _updateThemeFromCurrentTrack(MediaItem mediaItem) async {
|
||||||
if (_container == null) return;
|
if (_container == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
TrackMetadata? metadata;
|
||||||
|
|
||||||
// For remote tracks, get metadata from database
|
// For remote tracks, get metadata from database
|
||||||
final urlResolver = _container!.read(remoteUrlResolverProvider);
|
final urlResolver = _container!.read(remoteUrlResolverProvider);
|
||||||
if (urlResolver.isProtocolUrl(mediaItem.id)) {
|
if (urlResolver.isProtocolUrl(mediaItem.id)) {
|
||||||
@@ -60,22 +66,59 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
|
|||||||
database.tracks,
|
database.tracks,
|
||||||
)..where((t) => t.path.equals(mediaItem.id))).getSingleOrNull();
|
)..where((t) => t.path.equals(mediaItem.id))).getSingleOrNull();
|
||||||
|
|
||||||
if (track != null && track.artUri != null) {
|
if (track != null) {
|
||||||
// Fetch album art bytes for remote tracks
|
// Fetch album art bytes for remote tracks
|
||||||
// TODO: Implement remote album art fetching for theme
|
Uint8List? artBytes;
|
||||||
|
if (track.artUri != null) {
|
||||||
|
try {
|
||||||
|
final response = await http.get(Uri.parse(track.artUri!));
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
artBytes = response.bodyBytes;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore art fetching errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = TrackMetadata(
|
||||||
|
title: track.title,
|
||||||
|
artist: track.artist,
|
||||||
|
album: track.album,
|
||||||
|
artBytes: artBytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update theme from album art
|
||||||
|
final seedColorNotifier = _container!.read(
|
||||||
|
seedColorProvider.notifier,
|
||||||
|
);
|
||||||
|
seedColorNotifier.updateFromAlbumArtBytes(artBytes);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For local tracks, use existing metadata service
|
// For local tracks, use metadata service
|
||||||
// TODO: Get metadata service working
|
final metadataService = MetadataService();
|
||||||
|
metadata = await metadataService.getMetadata(mediaItem.id);
|
||||||
|
|
||||||
|
// Update theme from album art
|
||||||
|
final seedColorNotifier = _container!.read(seedColorProvider.notifier);
|
||||||
|
seedColorNotifier.updateFromAlbumArtBytes(metadata.artBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset to default for now
|
// Set current track metadata
|
||||||
final seedColorNotifier = _container!.read(seedColorProvider.notifier);
|
if (metadata != null) {
|
||||||
seedColorNotifier.resetToDefault();
|
final metadataNotifier = _container!.read(
|
||||||
|
currentTrackMetadataProvider.notifier,
|
||||||
|
);
|
||||||
|
metadataNotifier.setMetadata(metadata);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If metadata retrieval fails, reset to default color
|
// If metadata retrieval fails, reset to default color and clear metadata
|
||||||
final seedColorNotifier = _container!.read(seedColorProvider.notifier);
|
final seedColorNotifier = _container!.read(seedColorProvider.notifier);
|
||||||
seedColorNotifier.resetToDefault();
|
seedColorNotifier.resetToDefault();
|
||||||
|
|
||||||
|
final metadataNotifier = _container!.read(
|
||||||
|
currentTrackMetadataProvider.notifier,
|
||||||
|
);
|
||||||
|
metadataNotifier.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_media_metadata/flutter_media_metadata.dart';
|
import 'package:flutter_media_metadata/flutter_media_metadata.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
import 'package:groovybox/providers/remote_provider.dart';
|
|
||||||
import 'package:groovybox/providers/db_provider.dart';
|
import 'package:groovybox/providers/db_provider.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
class TrackMetadata {
|
class TrackMetadata {
|
||||||
final String? title;
|
final String? title;
|
||||||
@@ -38,20 +37,16 @@ class MetadataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
final trackMetadataProvider = FutureProvider.family<TrackMetadata, String>((
|
||||||
MetadataService metadataService(Ref ref) {
|
ref,
|
||||||
return MetadataService();
|
path,
|
||||||
}
|
) async {
|
||||||
|
try {
|
||||||
|
// Import the database provider directly
|
||||||
|
final db = ref.watch(databaseProvider);
|
||||||
|
|
||||||
@riverpod
|
final track = await (db.select(
|
||||||
Future<TrackMetadata> trackMetadata(Ref ref, String path) async {
|
db.tracks,
|
||||||
// Check if this is a remote track (protocol URL)
|
|
||||||
final urlResolver = ref.watch(remoteUrlResolverProvider);
|
|
||||||
if (urlResolver.isProtocolUrl(path)) {
|
|
||||||
// For remote tracks, get metadata from database
|
|
||||||
final database = ref.watch(databaseProvider);
|
|
||||||
final track = await (database.select(
|
|
||||||
database.tracks,
|
|
||||||
)..where((t) => t.path.equals(path))).getSingleOrNull();
|
)..where((t) => t.path.equals(path))).getSingleOrNull();
|
||||||
|
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
@@ -75,11 +70,21 @@ Future<TrackMetadata> trackMetadata(Ref ref, String path) async {
|
|||||||
album: track.album,
|
album: track.album,
|
||||||
artBytes: artBytes,
|
artBytes: artBytes,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return TrackMetadata(
|
||||||
|
title: 'Unknown Title',
|
||||||
|
artist: 'Unknown Artist',
|
||||||
|
album: 'Unknown Album',
|
||||||
|
artBytes: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return TrackMetadata();
|
} catch (e) {
|
||||||
} else {
|
debugPrint('Error fetching metadata for $path: $e');
|
||||||
// For local tracks, use file metadata
|
return TrackMetadata(
|
||||||
final service = MetadataService();
|
title: 'Unknown Title',
|
||||||
return service.getMetadata(path);
|
artist: 'Unknown Artist',
|
||||||
|
album: 'Unknown Album',
|
||||||
|
artBytes: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:groovybox/logic/audio_handler.dart';
|
import 'package:groovybox/logic/audio_handler.dart';
|
||||||
|
import 'package:groovybox/logic/metadata_service.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'audio_provider.g.dart';
|
part 'audio_provider.g.dart';
|
||||||
@@ -15,3 +16,19 @@ AudioHandler audioHandler(Ref ref) {
|
|||||||
void setAudioHandler(AudioHandler handler) {
|
void setAudioHandler(AudioHandler handler) {
|
||||||
_audioHandler = handler;
|
_audioHandler = handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class CurrentTrackMetadataNotifier extends _$CurrentTrackMetadataNotifier {
|
||||||
|
@override
|
||||||
|
TrackMetadata? build() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMetadata(TrackMetadata metadata) {
|
||||||
|
state = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
state = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,3 +49,58 @@ final class AudioHandlerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$audioHandlerHash() => r'65fbd92e049fe4f3a0763516f1e68e1614f7630f';
|
String _$audioHandlerHash() => r'65fbd92e049fe4f3a0763516f1e68e1614f7630f';
|
||||||
|
|
||||||
|
@ProviderFor(CurrentTrackMetadataNotifier)
|
||||||
|
const currentTrackMetadataProvider = CurrentTrackMetadataNotifierProvider._();
|
||||||
|
|
||||||
|
final class CurrentTrackMetadataNotifierProvider
|
||||||
|
extends $NotifierProvider<CurrentTrackMetadataNotifier, TrackMetadata?> {
|
||||||
|
const CurrentTrackMetadataNotifierProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'currentTrackMetadataProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$currentTrackMetadataNotifierHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
CurrentTrackMetadataNotifier create() => CurrentTrackMetadataNotifier();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(TrackMetadata? value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<TrackMetadata?>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$currentTrackMetadataNotifierHash() =>
|
||||||
|
r'0a491bd4edda2b010ed3d6f7dd459f4ac8689a5f';
|
||||||
|
|
||||||
|
abstract class _$CurrentTrackMetadataNotifier
|
||||||
|
extends $Notifier<TrackMetadata?> {
|
||||||
|
TrackMetadata? build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<TrackMetadata?, TrackMetadata?>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<TrackMetadata?, TrackMetadata?>,
|
||||||
|
TrackMetadata?,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,46 +48,37 @@ class PlayerScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
final media = medias[index];
|
final media = medias[index];
|
||||||
|
|
||||||
final path = Uri.decodeFull(Uri.parse(media.uri).path);
|
final currentMetadata = ref.watch(currentTrackMetadataProvider);
|
||||||
// For now, skip metadata loading to avoid provider issues
|
|
||||||
final AsyncValue<TrackMetadata> metadataAsync = AsyncValue.data(
|
|
||||||
TrackMetadata(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Build blurred background if cover art is available
|
// Build blurred background if cover art is available
|
||||||
Widget? background;
|
Widget? background;
|
||||||
metadataAsync.when(
|
final artBytes = currentMetadata?.artBytes;
|
||||||
data: (meta) {
|
if (artBytes != null) {
|
||||||
if (meta.artBytes != null) {
|
background = Positioned.fill(
|
||||||
background = Positioned.fill(
|
child: Stack(
|
||||||
child: Stack(
|
children: [
|
||||||
children: [
|
Container(
|
||||||
Container(
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
image: DecorationImage(
|
||||||
image: DecorationImage(
|
image: MemoryImage(artBytes),
|
||||||
image: MemoryImage(meta.artBytes!),
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
BackdropFilter(
|
),
|
||||||
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
|
||||||
child: Container(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.surface.withValues(alpha: 0.6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
BackdropFilter(
|
||||||
} else {
|
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
||||||
background = null;
|
child: Container(
|
||||||
}
|
color: Theme.of(
|
||||||
},
|
context,
|
||||||
loading: () => background = null,
|
).colorScheme.surface.withValues(alpha: 0.6),
|
||||||
error: (_, _) => background = null,
|
),
|
||||||
);
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
background = null;
|
||||||
|
}
|
||||||
|
|
||||||
final devicePadding = MediaQuery.paddingOf(context);
|
final devicePadding = MediaQuery.paddingOf(context);
|
||||||
|
|
||||||
@@ -116,7 +107,7 @@ class PlayerScreen extends HookConsumerWidget {
|
|||||||
body: ClipRect(
|
body: ClipRect(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
...background != null ? [background!] : [],
|
...background != null ? [background] : [],
|
||||||
// Main content (StreamBuilder)
|
// Main content (StreamBuilder)
|
||||||
Builder(
|
Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@@ -126,18 +117,16 @@ class PlayerScreen extends HookConsumerWidget {
|
|||||||
child: _MobileLayout(
|
child: _MobileLayout(
|
||||||
player: player,
|
player: player,
|
||||||
viewMode: viewMode,
|
viewMode: viewMode,
|
||||||
metadataAsync: metadataAsync,
|
|
||||||
media: media,
|
media: media,
|
||||||
trackPath: path,
|
trackPath: media.uri,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return _DesktopLayout(
|
return _DesktopLayout(
|
||||||
player: player,
|
player: player,
|
||||||
viewMode: viewMode,
|
viewMode: viewMode,
|
||||||
metadataAsync: metadataAsync,
|
|
||||||
media: media,
|
media: media,
|
||||||
trackPath: path,
|
trackPath: media.uri,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -164,30 +153,30 @@ class PlayerScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MobileLayout extends StatelessWidget {
|
class _MobileLayout extends HookConsumerWidget {
|
||||||
final Player player;
|
final Player player;
|
||||||
final ValueNotifier<ViewMode> viewMode;
|
final ValueNotifier<ViewMode> viewMode;
|
||||||
final AsyncValue<TrackMetadata> metadataAsync;
|
|
||||||
final Media media;
|
final Media media;
|
||||||
final String trackPath;
|
final String trackPath;
|
||||||
|
|
||||||
const _MobileLayout({
|
const _MobileLayout({
|
||||||
required this.player,
|
required this.player,
|
||||||
required this.viewMode,
|
required this.viewMode,
|
||||||
required this.metadataAsync,
|
|
||||||
required this.media,
|
required this.media,
|
||||||
required this.trackPath,
|
required this.trackPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final currentMetadata = ref.watch(currentTrackMetadataProvider);
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
return AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: switch (viewMode.value) {
|
child: switch (viewMode.value) {
|
||||||
ViewMode.cover => _CoverView(
|
ViewMode.cover => _CoverView(
|
||||||
key: const ValueKey('cover'),
|
key: const ValueKey('cover'),
|
||||||
player: player,
|
player: player,
|
||||||
metadataAsync: metadataAsync,
|
currentMetadata: currentMetadata,
|
||||||
media: media,
|
media: media,
|
||||||
trackPath: trackPath,
|
trackPath: trackPath,
|
||||||
).padding(bottom: MediaQuery.paddingOf(context).bottom),
|
).padding(bottom: MediaQuery.paddingOf(context).bottom),
|
||||||
@@ -207,13 +196,13 @@ class _MobileLayout extends StatelessWidget {
|
|||||||
|
|
||||||
class _PlayerCoverControlsPanel extends StatelessWidget {
|
class _PlayerCoverControlsPanel extends StatelessWidget {
|
||||||
final Player player;
|
final Player player;
|
||||||
final AsyncValue<TrackMetadata> metadataAsync;
|
final TrackMetadata? currentMetadata;
|
||||||
final Media media;
|
final Media media;
|
||||||
final String trackPath;
|
final String trackPath;
|
||||||
|
|
||||||
const _PlayerCoverControlsPanel({
|
const _PlayerCoverControlsPanel({
|
||||||
required this.player,
|
required this.player,
|
||||||
required this.metadataAsync,
|
required this.currentMetadata,
|
||||||
required this.media,
|
required this.media,
|
||||||
required this.trackPath,
|
required this.trackPath,
|
||||||
});
|
});
|
||||||
@@ -235,7 +224,7 @@ class _PlayerCoverControlsPanel extends StatelessWidget {
|
|||||||
constraints: const BoxConstraints(maxWidth: 400),
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: _PlayerCoverArt(metadataAsync: metadataAsync),
|
child: _PlayerCoverArt(currentMetadata: currentMetadata),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -243,7 +232,7 @@ class _PlayerCoverControlsPanel extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
_PlayerControls(
|
_PlayerControls(
|
||||||
player: player,
|
player: player,
|
||||||
metadataAsync: metadataAsync,
|
currentMetadata: currentMetadata,
|
||||||
media: media,
|
media: media,
|
||||||
trackPath: trackPath,
|
trackPath: trackPath,
|
||||||
),
|
),
|
||||||
@@ -254,23 +243,23 @@ class _PlayerCoverControlsPanel extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DesktopLayout extends StatelessWidget {
|
class _DesktopLayout extends HookConsumerWidget {
|
||||||
final Player player;
|
final Player player;
|
||||||
final ValueNotifier<ViewMode> viewMode;
|
final ValueNotifier<ViewMode> viewMode;
|
||||||
final AsyncValue<TrackMetadata> metadataAsync;
|
|
||||||
final Media media;
|
final Media media;
|
||||||
final String trackPath;
|
final String trackPath;
|
||||||
|
|
||||||
const _DesktopLayout({
|
const _DesktopLayout({
|
||||||
required this.player,
|
required this.player,
|
||||||
required this.viewMode,
|
required this.viewMode,
|
||||||
required this.metadataAsync,
|
|
||||||
required this.media,
|
required this.media,
|
||||||
required this.trackPath,
|
required this.trackPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final currentMetadata = ref.watch(currentTrackMetadataProvider);
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
return AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: switch (viewMode.value) {
|
child: switch (viewMode.value) {
|
||||||
@@ -278,7 +267,7 @@ class _DesktopLayout extends StatelessWidget {
|
|||||||
key: const ValueKey('cover'),
|
key: const ValueKey('cover'),
|
||||||
child: _PlayerCoverControlsPanel(
|
child: _PlayerCoverControlsPanel(
|
||||||
player: player,
|
player: player,
|
||||||
metadataAsync: metadataAsync,
|
currentMetadata: currentMetadata,
|
||||||
media: media,
|
media: media,
|
||||||
trackPath: trackPath,
|
trackPath: trackPath,
|
||||||
),
|
),
|
||||||
@@ -294,7 +283,7 @@ class _DesktopLayout extends StatelessWidget {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: _PlayerCoverControlsPanel(
|
child: _PlayerCoverControlsPanel(
|
||||||
player: player,
|
player: player,
|
||||||
metadataAsync: metadataAsync,
|
currentMetadata: currentMetadata,
|
||||||
media: media,
|
media: media,
|
||||||
trackPath: trackPath,
|
trackPath: trackPath,
|
||||||
),
|
),
|
||||||
@@ -328,7 +317,7 @@ class _DesktopLayout extends StatelessWidget {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: _PlayerCoverControlsPanel(
|
child: _PlayerCoverControlsPanel(
|
||||||
player: player,
|
player: player,
|
||||||
metadataAsync: metadataAsync,
|
currentMetadata: currentMetadata,
|
||||||
media: media,
|
media: media,
|
||||||
trackPath: trackPath,
|
trackPath: trackPath,
|
||||||
),
|
),
|
||||||
@@ -355,14 +344,14 @@ class _DesktopLayout extends StatelessWidget {
|
|||||||
|
|
||||||
class _CoverView extends StatelessWidget {
|
class _CoverView extends StatelessWidget {
|
||||||
final Player player;
|
final Player player;
|
||||||
final AsyncValue<TrackMetadata> metadataAsync;
|
final TrackMetadata? currentMetadata;
|
||||||
final Media media;
|
final Media media;
|
||||||
final String trackPath;
|
final String trackPath;
|
||||||
|
|
||||||
const _CoverView({
|
const _CoverView({
|
||||||
super.key,
|
super.key,
|
||||||
required this.player,
|
required this.player,
|
||||||
required this.metadataAsync,
|
required this.currentMetadata,
|
||||||
required this.media,
|
required this.media,
|
||||||
required this.trackPath,
|
required this.trackPath,
|
||||||
});
|
});
|
||||||
@@ -377,13 +366,13 @@ class _CoverView extends StatelessWidget {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 32),
|
padding: const EdgeInsets.only(bottom: 32),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: _PlayerCoverArt(metadataAsync: metadataAsync),
|
child: _PlayerCoverArt(currentMetadata: currentMetadata),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_PlayerControls(
|
_PlayerControls(
|
||||||
player: player,
|
player: player,
|
||||||
metadataAsync: metadataAsync,
|
currentMetadata: currentMetadata,
|
||||||
media: media,
|
media: media,
|
||||||
trackPath: trackPath,
|
trackPath: trackPath,
|
||||||
),
|
),
|
||||||
@@ -417,64 +406,49 @@ class _LyricsView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PlayerCoverArt extends StatelessWidget {
|
class _PlayerCoverArt extends StatelessWidget {
|
||||||
final AsyncValue<TrackMetadata> metadataAsync;
|
final TrackMetadata? currentMetadata;
|
||||||
|
|
||||||
const _PlayerCoverArt({required this.metadataAsync});
|
const _PlayerCoverArt({required this.currentMetadata});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return metadataAsync.when(
|
return Center(
|
||||||
data: (meta) => Center(
|
child: AspectRatio(
|
||||||
child: AspectRatio(
|
aspectRatio: 1,
|
||||||
aspectRatio: 1,
|
child: Container(
|
||||||
child: Container(
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
borderRadius: BorderRadius.circular(24),
|
||||||
borderRadius: BorderRadius.circular(24),
|
boxShadow: [
|
||||||
boxShadow: [
|
BoxShadow(
|
||||||
BoxShadow(
|
color: Theme.of(
|
||||||
color: Theme.of(
|
context,
|
||||||
context,
|
).colorScheme.shadow.withValues(alpha: 0.3),
|
||||||
).colorScheme.shadow.withValues(alpha: 0.3),
|
blurRadius: 20,
|
||||||
blurRadius: 20,
|
offset: const Offset(0, 10),
|
||||||
offset: const Offset(0, 10),
|
),
|
||||||
),
|
],
|
||||||
],
|
image: () {
|
||||||
image: meta.artBytes != null
|
final artBytes = currentMetadata?.artBytes;
|
||||||
|
return artBytes != null
|
||||||
? DecorationImage(
|
? DecorationImage(
|
||||||
image: MemoryImage(meta.artBytes!),
|
image: MemoryImage(artBytes),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
: null,
|
: null;
|
||||||
),
|
}(),
|
||||||
child: meta.artBytes == null
|
|
||||||
? Center(
|
|
||||||
child: Icon(
|
|
||||||
Icons.music_note,
|
|
||||||
size: 80,
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.7),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
|
||||||
error: (_, _) => Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Icon(
|
|
||||||
Icons.error_outline,
|
|
||||||
size: 80,
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.7),
|
|
||||||
),
|
),
|
||||||
|
child: currentMetadata?.artBytes == null
|
||||||
|
? Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.music_note,
|
||||||
|
size: 80,
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1161,7 +1135,7 @@ class _QueueView extends HookConsumerWidget {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final media = playlist.medias[index];
|
final media = playlist.medias[index];
|
||||||
final isCurrent = index == playlist.index;
|
final isCurrent = index == playlist.index;
|
||||||
final trackPath = Uri.decodeFull(Uri.parse(media.uri).path);
|
final trackPath = media.uri;
|
||||||
final trackAsync = ref.watch(trackByPathProvider(trackPath));
|
final trackAsync = ref.watch(trackByPathProvider(trackPath));
|
||||||
|
|
||||||
return trackAsync.when(
|
return trackAsync.when(
|
||||||
@@ -1525,13 +1499,13 @@ class _TimedLyricsView extends HookConsumerWidget {
|
|||||||
|
|
||||||
class _PlayerControls extends HookWidget {
|
class _PlayerControls extends HookWidget {
|
||||||
final Player player;
|
final Player player;
|
||||||
final AsyncValue<TrackMetadata> metadataAsync;
|
final TrackMetadata? currentMetadata;
|
||||||
final Media media;
|
final Media media;
|
||||||
final String trackPath;
|
final String trackPath;
|
||||||
|
|
||||||
const _PlayerControls({
|
const _PlayerControls({
|
||||||
required this.player,
|
required this.player,
|
||||||
required this.metadataAsync,
|
required this.currentMetadata,
|
||||||
required this.media,
|
required this.media,
|
||||||
required this.trackPath,
|
required this.trackPath,
|
||||||
});
|
});
|
||||||
@@ -1547,27 +1521,19 @@ class _PlayerControls extends HookWidget {
|
|||||||
// Title & Artist
|
// Title & Artist
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
metadataAsync.when(
|
Text(
|
||||||
data: (meta) => Text(
|
currentMetadata?.title ?? Uri.parse(media.uri).pathSegments.last,
|
||||||
meta.title ?? Uri.parse(media.uri).pathSegments.last,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
textAlign: TextAlign.center,
|
||||||
textAlign: TextAlign.center,
|
maxLines: 1,
|
||||||
maxLines: 1,
|
overflow: TextOverflow.ellipsis,
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
loading: () => const SizedBox(height: 32),
|
|
||||||
error: (_, _) => Text(Uri.parse(media.uri).pathSegments.last),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
metadataAsync.when(
|
Text(
|
||||||
data: (meta) => Text(
|
currentMetadata?.artist ?? 'Unknown Artist',
|
||||||
meta.artist ?? 'Unknown Artist',
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
color: Theme.of(context).colorScheme.primary,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
loading: () => const SizedBox(height: 24),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:groovybox/data/db.dart' as db;
|
import 'package:groovybox/data/db.dart' as db;
|
||||||
import 'package:groovybox/logic/metadata_service.dart';
|
|
||||||
import 'package:groovybox/providers/audio_provider.dart';
|
import 'package:groovybox/providers/audio_provider.dart';
|
||||||
import 'package:groovybox/ui/screens/player_screen.dart';
|
import 'package:groovybox/ui/screens/player_screen.dart';
|
||||||
import 'package:groovybox/ui/widgets/track_tile.dart';
|
import 'package:groovybox/ui/widgets/track_tile.dart';
|
||||||
@@ -53,10 +53,7 @@ class _MobileMiniPlayer extends HookConsumerWidget {
|
|||||||
|
|
||||||
final devicePadding = MediaQuery.paddingOf(context);
|
final devicePadding = MediaQuery.paddingOf(context);
|
||||||
|
|
||||||
// For now, skip metadata loading to avoid provider issues
|
final currentMetadata = ref.watch(currentTrackMetadataProvider);
|
||||||
final AsyncValue<TrackMetadata> metadataAsync = AsyncValue.data(
|
|
||||||
TrackMetadata(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget content = Container(
|
Widget content = Container(
|
||||||
height: 72 + devicePadding.bottom,
|
height: 72 + devicePadding.bottom,
|
||||||
@@ -132,19 +129,18 @@ class _MobileMiniPlayer extends HookConsumerWidget {
|
|||||||
// Cover Art
|
// Cover Art
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: metadataAsync.when(
|
child: currentMetadata?.artBytes != null
|
||||||
data: (meta) => meta.artBytes != null
|
? Image.memory(
|
||||||
? Image.memory(meta.artBytes!, fit: BoxFit.cover)
|
currentMetadata!.artBytes!,
|
||||||
: Container(
|
fit: BoxFit.cover,
|
||||||
color: Colors.grey[800],
|
)
|
||||||
child: const Icon(
|
: Container(
|
||||||
Icons.music_note,
|
color: Colors.grey[800],
|
||||||
color: Colors.white54,
|
child: const Icon(
|
||||||
),
|
Icons.music_note,
|
||||||
|
color: Colors.white54,
|
||||||
),
|
),
|
||||||
loading: () => Container(color: Colors.grey[800]),
|
),
|
||||||
error: (_, _) => Container(color: Colors.grey[800]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
// Title & Artist
|
// Title & Artist
|
||||||
@@ -155,28 +151,19 @@ class _MobileMiniPlayer extends HookConsumerWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
metadataAsync.when(
|
Text(
|
||||||
data: (meta) => Text(
|
currentMetadata?.title ??
|
||||||
meta.title ??
|
Uri.parse(media.uri).pathSegments.last,
|
||||||
Uri.parse(media.uri).pathSegments.last,
|
style: Theme.of(context).textTheme.bodyMedium
|
||||||
style: Theme.of(context).textTheme.bodyMedium
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
?.copyWith(fontWeight: FontWeight.bold),
|
maxLines: 1,
|
||||||
maxLines: 1,
|
overflow: TextOverflow.ellipsis,
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
loading: () => const Text('Loading...'),
|
|
||||||
error: (_, _) =>
|
|
||||||
Text(Uri.parse(media.uri).pathSegments.last),
|
|
||||||
),
|
),
|
||||||
metadataAsync.when(
|
Text(
|
||||||
data: (meta) => Text(
|
currentMetadata?.artist ?? 'Unknown Artist',
|
||||||
meta.artist ?? 'Unknown Artist',
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
maxLines: 1,
|
||||||
maxLines: 1,
|
overflow: TextOverflow.ellipsis,
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
loading: () => const SizedBox.shrink(),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -272,10 +259,7 @@ class _DesktopMiniPlayer extends HookConsumerWidget {
|
|||||||
|
|
||||||
final devicePadding = MediaQuery.paddingOf(context);
|
final devicePadding = MediaQuery.paddingOf(context);
|
||||||
|
|
||||||
// For now, skip metadata loading to avoid provider issues
|
final currentMetadata = ref.watch(currentTrackMetadataProvider);
|
||||||
final AsyncValue<TrackMetadata> metadataAsync = AsyncValue.data(
|
|
||||||
TrackMetadata(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget content = Container(
|
Widget content = Container(
|
||||||
height: 72 + devicePadding.bottom,
|
height: 72 + devicePadding.bottom,
|
||||||
@@ -356,23 +340,18 @@ class _DesktopMiniPlayer extends HookConsumerWidget {
|
|||||||
// Cover Art
|
// Cover Art
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: metadataAsync.when(
|
child: currentMetadata?.artBytes != null
|
||||||
data: (meta) => meta.artBytes != null
|
? Image.memory(
|
||||||
? Image.memory(
|
currentMetadata!.artBytes!,
|
||||||
meta.artBytes!,
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
)
|
||||||
)
|
: Container(
|
||||||
: Container(
|
color: Colors.grey[800],
|
||||||
color: Colors.grey[800],
|
child: const Icon(
|
||||||
child: const Icon(
|
Icons.music_note,
|
||||||
Icons.music_note,
|
color: Colors.white54,
|
||||||
color: Colors.white54,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
loading: () => Container(color: Colors.grey[800]),
|
),
|
||||||
error: (_, _) =>
|
|
||||||
Container(color: Colors.grey[800]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
// Title & Artist
|
// Title & Artist
|
||||||
@@ -384,33 +363,19 @@ class _DesktopMiniPlayer extends HookConsumerWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
metadataAsync.when(
|
Text(
|
||||||
data: (meta) => Text(
|
currentMetadata?.title ??
|
||||||
meta.title ??
|
Uri.parse(media.uri).pathSegments.last,
|
||||||
Uri.parse(media.uri).pathSegments.last,
|
style: Theme.of(context).textTheme.bodyMedium
|
||||||
style: Theme.of(context)
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
.textTheme
|
maxLines: 1,
|
||||||
.bodyMedium
|
overflow: TextOverflow.ellipsis,
|
||||||
?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
loading: () => const Text('Loading...'),
|
|
||||||
error: (_, _) => Text(
|
|
||||||
Uri.parse(media.uri).pathSegments.last,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
metadataAsync.when(
|
Text(
|
||||||
data: (meta) => Text(
|
currentMetadata?.artist ?? 'Unknown Artist',
|
||||||
meta.artist ?? 'Unknown Artist',
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
style: Theme.of(
|
maxLines: 1,
|
||||||
context,
|
overflow: TextOverflow.ellipsis,
|
||||||
).textTheme.bodySmall,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
loading: () => const SizedBox.shrink(),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -537,7 +537,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.6"
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ dependencies:
|
|||||||
styled_widget: ^0.4.1
|
styled_widget: ^0.4.1
|
||||||
super_sliver_list: ^0.4.1
|
super_sliver_list: ^0.4.1
|
||||||
dio: ^5.0.0
|
dio: ^5.0.0
|
||||||
|
http: ^1.2.2
|
||||||
audio_service: ^0.18.18
|
audio_service: ^0.18.18
|
||||||
palette_generator: ^0.3.3+4
|
palette_generator: ^0.3.3+4
|
||||||
watcher: ^1.2.0
|
watcher: ^1.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user