♻️ Unified the track tile widget
This commit is contained in:
@@ -369,4 +369,42 @@ class TrackRepository extends _$TrackRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear all tracks from the database and delete associated files/art.
|
||||||
|
Future<void> clearAllTracks() async {
|
||||||
|
final db = ref.read(databaseProvider);
|
||||||
|
final appDir = await getApplicationDocumentsDirectory();
|
||||||
|
final musicDir = p.join(appDir.path, 'music');
|
||||||
|
|
||||||
|
// Get all tracks first
|
||||||
|
final allTracks = await db.select(db.tracks).get();
|
||||||
|
|
||||||
|
// Delete associated files and art for each track
|
||||||
|
for (final track in allTracks) {
|
||||||
|
// Delete file only if it's a copied file (in internal music directory)
|
||||||
|
final file = File(track.path);
|
||||||
|
if (await file.exists() && track.path.startsWith(musicDir)) {
|
||||||
|
try {
|
||||||
|
await file.delete();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error deleting file: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete album art if exists (always stored internally)
|
||||||
|
if (track.artUri != null) {
|
||||||
|
final artFile = File(track.artUri!);
|
||||||
|
if (await artFile.exists()) {
|
||||||
|
try {
|
||||||
|
await artFile.delete();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error deleting art: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all tracks from database (cascade will handle playlist entries)
|
||||||
|
await db.delete(db.tracks).go();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:groovybox/data/db.dart';
|
import 'package:groovybox/data/db.dart';
|
||||||
import 'package:groovybox/data/playlist_repository.dart';
|
import 'package:groovybox/data/playlist_repository.dart';
|
||||||
import 'package:groovybox/providers/audio_provider.dart';
|
import 'package:groovybox/providers/audio_provider.dart';
|
||||||
|
import 'package:groovybox/ui/widgets/track_tile.dart';
|
||||||
|
import 'package:groovybox/ui/widgets/universal_image.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
class AlbumDetailScreen extends HookConsumerWidget {
|
class AlbumDetailScreen extends HookConsumerWidget {
|
||||||
final AlbumData album;
|
final AlbumData album;
|
||||||
@@ -24,7 +26,7 @@ class AlbumDetailScreen extends HookConsumerWidget {
|
|||||||
flexibleSpace: FlexibleSpaceBar(
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
title: Text(album.album),
|
title: Text(album.album),
|
||||||
background: album.artUri != null
|
background: album.artUri != null
|
||||||
? Image.file(File(album.artUri!), fit: BoxFit.cover)
|
? UniversalImage(uri: album.artUri!, fit: BoxFit.cover)
|
||||||
: Container(
|
: Container(
|
||||||
color: Colors.grey[800],
|
color: Colors.grey[800],
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@@ -85,17 +87,17 @@ class AlbumDetailScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget _buildTrackTile(WidgetRef ref, List<Track> tracks, int index) {
|
Widget _buildTrackTile(WidgetRef ref, List<Track> tracks, int index) {
|
||||||
final track = tracks[index];
|
final track = tracks[index];
|
||||||
return ListTile(
|
return TrackTile(
|
||||||
|
track: track,
|
||||||
leading: Text(
|
leading: Text(
|
||||||
'${index + 1}',
|
'${index + 1}'.padLeft(2, '0'),
|
||||||
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
||||||
),
|
).padding(right: 16),
|
||||||
title: Text(track.title),
|
showTrailingIcon: false,
|
||||||
subtitle: Text(_formatDuration(track.duration)),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_playAlbum(ref, tracks, initialIndex: index);
|
_playAlbum(ref, tracks, initialIndex: index);
|
||||||
},
|
},
|
||||||
trailing: const Icon(Icons.play_circle_outline),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,12 +105,4 @@ class AlbumDetailScreen extends HookConsumerWidget {
|
|||||||
final audioHandler = ref.read(audioHandlerProvider);
|
final audioHandler = ref.read(audioHandlerProvider);
|
||||||
audioHandler.playTracks(tracks, initialIndex: initialIndex);
|
audioHandler.playTracks(tracks, 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,5 +1,4 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
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';
|
||||||
@@ -9,12 +8,11 @@ import 'package:groovybox/data/playlist_repository.dart';
|
|||||||
import 'package:groovybox/data/track_repository.dart';
|
import 'package:groovybox/data/track_repository.dart';
|
||||||
import 'package:groovybox/logic/lyrics_parser.dart';
|
import 'package:groovybox/logic/lyrics_parser.dart';
|
||||||
import 'package:groovybox/providers/audio_provider.dart';
|
import 'package:groovybox/providers/audio_provider.dart';
|
||||||
import 'package:groovybox/providers/remote_provider.dart';
|
|
||||||
import 'package:groovybox/providers/watch_folder_provider.dart';
|
import 'package:groovybox/providers/watch_folder_provider.dart';
|
||||||
import 'package:groovybox/ui/screens/settings_screen.dart';
|
import 'package:groovybox/ui/screens/settings_screen.dart';
|
||||||
import 'package:groovybox/ui/tabs/albums_tab.dart';
|
import 'package:groovybox/ui/tabs/albums_tab.dart';
|
||||||
import 'package:groovybox/ui/tabs/playlists_tab.dart';
|
import 'package:groovybox/ui/tabs/playlists_tab.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:groovybox/ui/widgets/track_tile.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@@ -483,37 +481,19 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
SnackBar(content: Text('Deleted "${track.title}"')),
|
SnackBar(content: Text('Deleted "${track.title}"')),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: ListTile(
|
child: TrackTile(
|
||||||
leading: AspectRatio(
|
track: track,
|
||||||
aspectRatio: 1,
|
showTrailingIcon: true,
|
||||||
child: _buildAlbumArt(track, ref),
|
onTrailingPressed: () =>
|
||||||
),
|
_showTrackOptions(context, ref, track),
|
||||||
title: Text(
|
|
||||||
track.title,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
'${track.artist ?? 'Unknown Artist'} • ${_formatDuration(track.duration)}',
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
trailing: isSelectionMode
|
|
||||||
? null
|
|
||||||
: IconButton(
|
|
||||||
icon: const Icon(Icons.more_vert),
|
|
||||||
onPressed: () {
|
|
||||||
_showTrackOptions(context, ref, track);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final audio = ref.read(audioHandlerProvider);
|
final audio = ref.read(audioHandlerProvider);
|
||||||
audio.playTrack(track);
|
audio.playTrack(track);
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
padding: const EdgeInsets.symmetric(
|
||||||
// Enter selection mode
|
horizontal: 16,
|
||||||
toggleSelection(track.id);
|
vertical: 8,
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -853,81 +833,6 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAlbumArt(Track track, WidgetRef ref) {
|
|
||||||
// Check if this is a remote track
|
|
||||||
final urlResolver = ref.watch(remoteUrlResolverProvider);
|
|
||||||
final isRemote = urlResolver.isProtocolUrl(track.path);
|
|
||||||
|
|
||||||
if (isRemote && track.artUri != null) {
|
|
||||||
// For remote tracks, fetch album art directly
|
|
||||||
return FutureBuilder<Uint8List?>(
|
|
||||||
future: _fetchRemoteAlbumArt(track.artUri!),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return Container(
|
|
||||||
color: Colors.grey[800],
|
|
||||||
child: const Center(
|
|
||||||
child: SizedBox(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white54),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (snapshot.hasData && snapshot.data != null) {
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[800],
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
image: DecorationImage(
|
|
||||||
image: MemoryImage(snapshot.data!),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Container(
|
|
||||||
color: Colors.grey[800],
|
|
||||||
child: const Icon(Icons.music_note, color: Colors.white54),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// For local tracks, use existing logic
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[800],
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
image: track.artUri != null
|
|
||||||
? DecorationImage(
|
|
||||||
image: FileImage(File(track.artUri!)),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
child: track.artUri == null
|
|
||||||
? const Icon(Icons.music_note, color: Colors.white54)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Uint8List?> _fetchRemoteAlbumArt(String url) async {
|
|
||||||
try {
|
|
||||||
final response = await http.get(Uri.parse(url));
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
return response.bodyBytes;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatDuration(int? durationMs) {
|
String _formatDuration(int? durationMs) {
|
||||||
if (durationMs == null) return '--:--';
|
if (durationMs == null) return '--:--';
|
||||||
final d = Duration(milliseconds: durationMs);
|
final d = Duration(milliseconds: durationMs);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:groovybox/data/db.dart';
|
import 'package:groovybox/data/db.dart';
|
||||||
import 'package:groovybox/data/playlist_repository.dart';
|
import 'package:groovybox/data/playlist_repository.dart';
|
||||||
import 'package:groovybox/providers/audio_provider.dart';
|
import 'package:groovybox/providers/audio_provider.dart';
|
||||||
|
import 'package:groovybox/ui/widgets/track_tile.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
class PlaylistDetailScreen extends HookConsumerWidget {
|
class PlaylistDetailScreen extends HookConsumerWidget {
|
||||||
@@ -93,17 +94,17 @@ class PlaylistDetailScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget _buildTrackTile(WidgetRef ref, List<Track> tracks, int index) {
|
Widget _buildTrackTile(WidgetRef ref, List<Track> tracks, int index) {
|
||||||
final track = tracks[index];
|
final track = tracks[index];
|
||||||
return ListTile(
|
return TrackTile(
|
||||||
|
track: track,
|
||||||
leading: Text(
|
leading: Text(
|
||||||
'${index + 1}',
|
'${index + 1}',
|
||||||
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
||||||
),
|
),
|
||||||
title: Text(track.title),
|
showTrailingIcon: false,
|
||||||
subtitle: Text(track.artist ?? 'Unknown Artist'),
|
|
||||||
trailing: Text(_formatDuration(track.duration)),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_playPlaylist(ref, tracks, initialIndex: index);
|
_playPlaylist(ref, tracks, initialIndex: index);
|
||||||
},
|
},
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,12 +116,4 @@ class PlaylistDetailScreen extends HookConsumerWidget {
|
|||||||
final audioHandler = ref.read(audioHandlerProvider);
|
final audioHandler = ref.read(audioHandlerProvider);
|
||||||
audioHandler.playTracks(tracks, initialIndex: initialIndex);
|
audioHandler.playTracks(tracks, 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,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:groovybox/data/track_repository.dart';
|
||||||
import 'package:groovybox/providers/settings_provider.dart';
|
import 'package:groovybox/providers/settings_provider.dart';
|
||||||
import 'package:groovybox/providers/watch_folder_provider.dart';
|
import 'package:groovybox/providers/watch_folder_provider.dart';
|
||||||
import 'package:groovybox/providers/remote_provider.dart';
|
import 'package:groovybox/providers/remote_provider.dart';
|
||||||
@@ -305,6 +306,42 @@ class SettingsScreen extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Database Management Section
|
||||||
|
Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Database Management',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).padding(horizontal: 16, bottom: 8, top: 16),
|
||||||
|
const Text(
|
||||||
|
'Manage your music database and cached files.',
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||||
|
).padding(horizontal: 16, bottom: 8),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Reset Track Database'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Remove all tracks from database and delete cached files. This action cannot be undone.',
|
||||||
|
),
|
||||||
|
trailing: ElevatedButton(
|
||||||
|
onPressed: () => _resetTrackDatabase(context, ref),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('Reset'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -491,4 +528,48 @@ class SettingsScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _resetTrackDatabase(BuildContext context, WidgetRef ref) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Reset Track Database'),
|
||||||
|
content: const Text(
|
||||||
|
'This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.of(context).pop(); // Close confirmation dialog
|
||||||
|
|
||||||
|
try {
|
||||||
|
final repository = ref.read(trackRepositoryProvider.notifier);
|
||||||
|
await repository.clearAllTracks();
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Track database has been reset'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error resetting database: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
child: const Text('Reset'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
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/data/playlist_repository.dart';
|
||||||
import 'package:groovybox/ui/screens/album_detail_screen.dart';
|
import 'package:groovybox/ui/screens/album_detail_screen.dart';
|
||||||
|
import 'package:groovybox/ui/widgets/universal_image.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
class AlbumsTab extends HookConsumerWidget {
|
class AlbumsTab extends HookConsumerWidget {
|
||||||
@@ -49,15 +49,11 @@ class AlbumsTab extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: album.artUri != null
|
child: UniversalImage(
|
||||||
? Image.file(File(album.artUri!), fit: BoxFit.cover)
|
uri: album.artUri,
|
||||||
: Container(
|
fit: BoxFit.cover,
|
||||||
color: Colors.grey[800],
|
fallbackIcon: Icons.album,
|
||||||
child: const Icon(
|
fallbackIconSize: 48,
|
||||||
Icons.album,
|
|
||||||
size: 48,
|
|
||||||
color: Colors.white54,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
@@ -355,7 +355,8 @@ class _DesktopMiniPlayer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
// Title & Artist
|
// Title & Artist
|
||||||
Padding(
|
Flexible(
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12.0,
|
horizontal: 12.0,
|
||||||
),
|
),
|
||||||
@@ -366,20 +367,25 @@ class _DesktopMiniPlayer extends HookConsumerWidget {
|
|||||||
Text(
|
Text(
|
||||||
currentMetadata?.title ??
|
currentMetadata?.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,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
currentMetadata?.artist ?? 'Unknown Artist',
|
currentMetadata?.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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:groovybox/data/db.dart' as db;
|
import 'package:groovybox/data/db.dart' as db;
|
||||||
|
import 'package:groovybox/ui/widgets/universal_image.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
class TrackTile extends StatelessWidget {
|
class TrackTile extends StatelessWidget {
|
||||||
final db.Track track;
|
final db.Track track;
|
||||||
@@ -41,29 +41,20 @@ class TrackTile extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
contentPadding:
|
contentPadding: padding ?? const EdgeInsets.symmetric(horizontal: 16),
|
||||||
padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
leading: Row(
|
leading: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
?leading,
|
?leading,
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: Container(
|
child: UniversalImage(
|
||||||
decoration: BoxDecoration(
|
uri: track.artUri,
|
||||||
color: Colors.grey[800],
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
image: track.artUri != null
|
|
||||||
? DecorationImage(
|
|
||||||
image: FileImage(File(track.artUri!)),
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
borderRadius: BorderRadius.circular(8),
|
||||||
: null,
|
fallbackIcon: Icons.music_note,
|
||||||
),
|
fallbackIconSize: 24,
|
||||||
child: track.artUri == null
|
).clipRRect(all: 8),
|
||||||
? const Icon(Icons.music_note, color: Colors.white54)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
130
lib/ui/widgets/universal_image.dart
Normal file
130
lib/ui/widgets/universal_image.dart
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class UniversalImage extends StatelessWidget {
|
||||||
|
final String? uri;
|
||||||
|
final BoxFit? fit;
|
||||||
|
final double? width;
|
||||||
|
final double? height;
|
||||||
|
final Widget? fallback;
|
||||||
|
final IconData? fallbackIcon;
|
||||||
|
final double? fallbackIconSize;
|
||||||
|
final Color? fallbackIconColor;
|
||||||
|
final BorderRadius? borderRadius;
|
||||||
|
final bool useDecorationImage;
|
||||||
|
|
||||||
|
const UniversalImage({
|
||||||
|
super.key,
|
||||||
|
this.uri,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.fallback,
|
||||||
|
this.fallbackIcon = Icons.image,
|
||||||
|
this.fallbackIconSize = 48,
|
||||||
|
this.fallbackIconColor = Colors.white54,
|
||||||
|
this.borderRadius,
|
||||||
|
this.useDecorationImage = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool _isNetworkUri(String uri) {
|
||||||
|
return uri.startsWith('http://') || uri.startsWith('https://');
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFallback() {
|
||||||
|
if (fallback != null) {
|
||||||
|
return fallback!;
|
||||||
|
}
|
||||||
|
|
||||||
|
final icon = Icon(
|
||||||
|
fallbackIcon,
|
||||||
|
size: fallbackIconSize,
|
||||||
|
color: fallbackIconColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (borderRadius != null) {
|
||||||
|
return Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[800],
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
),
|
||||||
|
child: icon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
color: Colors.grey[800],
|
||||||
|
child: icon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNetworkImage() {
|
||||||
|
if (useDecorationImage) {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: uri!,
|
||||||
|
fit: fit,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
placeholder: (context, url) => Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
color: Colors.grey[800],
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => _buildFallback(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: uri!,
|
||||||
|
fit: fit,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
placeholder: (context, url) => Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
color: Colors.grey[800],
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => _buildFallback(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFileImage() {
|
||||||
|
if (useDecorationImage) {
|
||||||
|
return Image.file(
|
||||||
|
File(uri!),
|
||||||
|
fit: fit,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
errorBuilder: (context, error, stackTrace) => _buildFallback(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Image.file(
|
||||||
|
File(uri!),
|
||||||
|
fit: fit,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
errorBuilder: (context, error, stackTrace) => _buildFallback(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (uri == null || uri!.isEmpty) {
|
||||||
|
return _buildFallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isNetworkUri(uri!)) {
|
||||||
|
return _buildNetworkImage();
|
||||||
|
} else {
|
||||||
|
return _buildFileImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
pubspec.lock
32
pubspec.lock
@@ -161,6 +161,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.12.1"
|
version: "8.12.1"
|
||||||
|
cached_network_image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cached_network_image
|
||||||
|
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
|
cached_network_image_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_platform_interface
|
||||||
|
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
|
cached_network_image_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_web
|
||||||
|
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -752,6 +776,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
octo_image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: octo_image
|
||||||
|
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ dependencies:
|
|||||||
watcher: ^1.2.0
|
watcher: ^1.2.0
|
||||||
shared_preferences: ^2.3.5
|
shared_preferences: ^2.3.5
|
||||||
jellyfin_dart: ^0.1.2
|
jellyfin_dart: ^0.1.2
|
||||||
|
cached_network_image: ^3.4.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user