🍱 Regenerate app icons

This commit is contained in:
2025-12-17 22:25:59 +08:00
parent c1652e6743
commit 935e77421e
58 changed files with 535 additions and 442 deletions

View File

@@ -7,7 +7,7 @@
<application
android:label="groovybox"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/launcher_icon">
<activity android:name="com.ryanheise.audioservice.AudioServiceActivity"></activity>
<service android:name="com.ryanheise.audioservice.AudioService"

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -429,7 +429,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -486,7 +486,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";

View File

@@ -1,122 +1 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
{"images":[{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@2x.png","scale":"2x","platform":"ios"},{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@3x.png","scale":"3x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@2x.png","scale":"2x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@3x.png","scale":"3x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@2x.png","scale":"2x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@3x.png","scale":"3x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@2x.png","scale":"2x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@3x.png","scale":"3x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@2x.png","scale":"2x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@3x.png","scale":"3x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@2x.png","scale":"2x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@3x.png","scale":"3x","platform":"ios"},{"size":"68x68","idiom":"universal","filename":"Icon-App-68x68@2x.png","scale":"2x","platform":"ios"},{"size":"76x76","idiom":"universal","filename":"Icon-App-76x76@2x.png","scale":"2x","platform":"ios"},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x","platform":"ios"},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-1024x1024@1x.png","scale":"1x","platform":"ios"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"68x68","idiom":"universal","filename":"Icon-App-Dark-68x68@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"76x76","idiom":"universal","filename":"Icon-App-Dark-76x76@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-Dark-83.5x83.5@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-Dark-1024x1024@1x.png","scale":"1x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]}],"info":{"version":1,"author":"xcode"}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -48,6 +48,8 @@ class LibraryScreen extends HookConsumerWidget {
final selectedTrackIds = useState<Set<int>>({});
final searchQuery = useState<String>('');
final isSelectionMode = selectedTrackIds.value.isNotEmpty;
final isLargeScreen = MediaQuery.of(context).size.width > 600;
final selectedTab = isLargeScreen ? useState<int>(0) : null;
void toggleSelection(int id) {
final newSet = Set<int>.from(selectedTrackIds.value);
@@ -63,9 +65,8 @@ class LibraryScreen extends HookConsumerWidget {
selectedTrackIds.value = {};
}
return DefaultTabController(
length: 3,
child: Scaffold(
if (isLargeScreen) {
return Scaffold(
appBar: isSelectionMode
? AppBar(
leading: IconButton(
@@ -105,13 +106,6 @@ class LibraryScreen extends HookConsumerWidget {
: AppBar(
centerTitle: true,
title: const Text('Library'),
bottom: const TabBar(
tabs: [
Tab(text: 'Tracks', icon: Icon(Icons.audiotrack)),
Tab(text: 'Albums', icon: Icon(Icons.album)),
Tab(text: 'Playlists', icon: Icon(Icons.queue_music)),
],
),
actions: [
IconButton(
icon: const Icon(Icons.add_circle_outline),
@@ -164,224 +158,391 @@ class LibraryScreen extends HookConsumerWidget {
const Gap(8),
],
),
body: TabBarView(
body: Row(
children: [
// Tracks Tab (Existing Logic)
StreamBuilder<List<Track>>(
stream: repo.watchAllTracks(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final tracks = snapshot.data!;
if (tracks.isEmpty) {
return const Center(child: Text('No tracks yet. Add some!'));
}
List<Track> filteredTracks;
if (searchQuery.value.isEmpty) {
filteredTracks = tracks;
} else {
final query = searchQuery.value.toLowerCase();
filteredTracks = tracks.where((track) {
if (track.title.toLowerCase().contains(query)) return true;
if (track.artist?.toLowerCase().contains(query) ?? false)
return true;
if (track.album?.toLowerCase().contains(query) ?? false)
return true;
if (track.lyrics != null) {
try {
final lyricsData = LyricsData.fromJsonString(
track.lyrics!,
);
for (final line in lyricsData.lines) {
if (line.text.toLowerCase().contains(query))
return true;
}
} catch (e) {
// Ignore parsing errors
}
}
return false;
}).toList();
}
if (filteredTracks.isEmpty && searchQuery.value.isNotEmpty) {
return const Center(
child: Text('No tracks match your search.'),
);
}
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
onChanged: (value) => searchQuery.value = value,
decoration: const InputDecoration(
hintText: 'Search tracks...',
prefixIcon: Icon(Icons.search),
),
),
),
Expanded(
child: ListView.builder(
padding: EdgeInsets.only(
bottom: 72 + MediaQuery.paddingOf(context).bottom,
),
itemCount: filteredTracks.length,
itemBuilder: (context, index) {
final track = filteredTracks[index];
final isSelected = selectedTrackIds.value.contains(
track.id,
);
if (isSelectionMode) {
return ListTile(
selected: isSelected,
selectedTileColor: Colors.white10,
leading: Checkbox(
value: isSelected,
onChanged: (_) => toggleSelection(track.id),
),
title: Text(
track.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'${track.artist ?? 'Unknown Artist'}${_formatDuration(track.duration)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => toggleSelection(track.id),
);
}
return Dismissible(
key: Key('track_${track.id}'),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(
Icons.delete,
color: Colors.white,
),
),
confirmDismiss: (direction) async {
return await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Delete Track?'),
content: Text(
'Are you sure you want to delete "${track.title}"? This cannot be undone.',
),
actions: [
TextButton(
onPressed: () =>
Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () =>
Navigator.of(context).pop(true),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
child: const Text('Delete'),
),
],
);
},
);
},
onDismissed: (direction) {
ref
.read(trackRepositoryProvider.notifier)
.deleteTrack(track.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Deleted "${track.title}"'),
),
);
},
child: ListTile(
leading: AspectRatio(
aspectRatio: 1,
child: 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,
),
),
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: () {
final audio = ref.read(audioHandlerProvider);
audio.playTrack(track);
},
onLongPress: () {
// Enter selection mode
toggleSelection(track.id);
},
),
);
},
),
),
],
);
},
NavigationRail(
selectedIndex: selectedTab!.value,
onDestinationSelected: (index) => selectedTab.value = index,
destinations: const [
NavigationRailDestination(
icon: Icon(Icons.audiotrack),
label: Text('Tracks'),
),
NavigationRailDestination(
icon: Icon(Icons.album),
label: Text('Albums'),
),
NavigationRailDestination(
icon: Icon(Icons.queue_music),
label: Text('Playlists'),
),
],
),
Expanded(
child: _buildTabContent(
selectedTab.value,
ref,
repo,
selectedTrackIds,
searchQuery,
toggleSelection,
isSelectionMode,
),
),
// Albums Tab
const AlbumsTab(),
// Playlists Tab
const PlaylistsTab(),
],
),
),
);
} else {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: isSelectionMode
? AppBar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: clearSelection,
),
title: Text('${selectedTrackIds.value.length} selected'),
backgroundColor: Theme.of(context).primaryColorDark,
actions: [
IconButton(
icon: const Icon(Icons.playlist_add),
tooltip: 'Add to Playlist',
onPressed: () {
_batchAddToPlaylist(
context,
ref,
selectedTrackIds.value.toList(),
clearSelection,
);
},
),
IconButton(
icon: const Icon(Icons.delete),
tooltip: 'Delete',
onPressed: () {
_batchDelete(
context,
ref,
selectedTrackIds.value.toList(),
clearSelection,
);
},
),
const Gap(8),
],
)
: AppBar(
centerTitle: true,
title: const Text('Library'),
bottom: const TabBar(
tabs: [
Tab(text: 'Tracks', icon: Icon(Icons.audiotrack)),
Tab(text: 'Albums', icon: Icon(Icons.album)),
Tab(text: 'Playlists', icon: Icon(Icons.queue_music)),
],
),
actions: [
IconButton(
icon: const Icon(Icons.add_circle_outline),
tooltip: 'Import Files',
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: allAllowedExtensions,
allowMultiple: true,
);
if (result != null && result.files.isNotEmpty) {
final paths = result.files
.map((f) => f.path)
.whereType<String>()
.toList();
if (paths.isNotEmpty) {
// 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,
);
}
}
}
},
),
const Gap(8),
],
),
body: TabBarView(
children: [
_buildTracksTab(
ref,
repo,
selectedTrackIds,
searchQuery,
toggleSelection,
isSelectionMode,
),
const AlbumsTab(),
const PlaylistsTab(),
],
),
),
);
}
}
Widget _buildTracksTab(
WidgetRef ref,
dynamic repo,
ValueNotifier<Set<int>> selectedTrackIds,
ValueNotifier<String> searchQuery,
void Function(int) toggleSelection,
bool isSelectionMode,
) {
return StreamBuilder<List<Track>>(
stream: repo.watchAllTracks(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final tracks = snapshot.data!;
if (tracks.isEmpty) {
return const Center(child: Text('No tracks yet. Add some!'));
}
List<Track> filteredTracks;
if (searchQuery.value.isEmpty) {
filteredTracks = tracks;
} else {
final query = searchQuery.value.toLowerCase();
filteredTracks = tracks.where((track) {
if (track.title.toLowerCase().contains(query)) return true;
if (track.artist?.toLowerCase().contains(query) ?? false)
return true;
if (track.album?.toLowerCase().contains(query) ?? false)
return true;
if (track.lyrics != null) {
try {
final lyricsData = LyricsData.fromJsonString(track.lyrics!);
for (final line in lyricsData.lines) {
if (line.text.toLowerCase().contains(query)) return true;
}
} catch (e) {
// Ignore parsing errors
}
}
return false;
}).toList();
}
if (filteredTracks.isEmpty && searchQuery.value.isNotEmpty) {
return const Center(child: Text('No tracks match your search.'));
}
return Column(
children: [
Padding(
padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width * 0.04,
vertical: 16.0,
),
child: TextField(
onChanged: (value) => searchQuery.value = value,
decoration: const InputDecoration(
hintText: 'Search tracks...',
prefixIcon: Icon(Icons.search),
),
),
),
Expanded(
child: ListView.builder(
padding: EdgeInsets.only(
bottom: 72 + MediaQuery.paddingOf(context).bottom,
),
itemCount: filteredTracks.length,
itemBuilder: (context, index) {
final track = filteredTracks[index];
final isSelected = selectedTrackIds.value.contains(track.id);
if (isSelectionMode) {
return ListTile(
selected: isSelected,
selectedTileColor: Colors.white10,
leading: Checkbox(
value: isSelected,
onChanged: (_) => toggleSelection(track.id),
),
title: Text(
track.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'${track.artist ?? 'Unknown Artist'}${_formatDuration(track.duration)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => toggleSelection(track.id),
);
}
return Dismissible(
key: Key('track_${track.id}'),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (direction) async {
return await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Delete Track?'),
content: Text(
'Are you sure you want to delete "${track.title}"? This cannot be undone.',
),
actions: [
TextButton(
onPressed: () =>
Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () =>
Navigator.of(context).pop(true),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
child: const Text('Delete'),
),
],
);
},
);
},
onDismissed: (direction) {
ref
.read(trackRepositoryProvider.notifier)
.deleteTrack(track.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Deleted "${track.title}"')),
);
},
child: ListTile(
leading: AspectRatio(
aspectRatio: 1,
child: 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,
),
),
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: () {
final audio = ref.read(audioHandlerProvider);
audio.playTrack(track);
},
onLongPress: () {
// Enter selection mode
toggleSelection(track.id);
},
),
);
},
),
),
],
);
},
);
}
Widget _buildTabContent(
int index,
WidgetRef ref,
dynamic repo,
ValueNotifier<Set<int>> selectedTrackIds,
ValueNotifier<String> searchQuery,
void Function(int) toggleSelection,
bool isSelectionMode,
) {
switch (index) {
case 0:
return _buildTracksTab(
ref,
repo,
selectedTrackIds,
searchQuery,
toggleSelection,
isSelectionMode,
);
case 1:
return const AlbumsTab();
case 2:
return const PlaylistsTab();
default:
return const SizedBox();
}
}
void _showTrackOptions(BuildContext context, WidgetRef ref, Track track) {
showModalBottomSheet(
context: context,
@@ -439,6 +600,7 @@ class LibraryScreen extends HookConsumerWidget {
WidgetRef ref,
Track track,
) {
final screenSize = MediaQuery.of(context).size;
showDialog(
context: context,
builder: (context) {
@@ -448,8 +610,11 @@ class LibraryScreen extends HookConsumerWidget {
// Or we can use a Consumer widget inside the dialog.
return AlertDialog(
title: const Text('Add to Playlist'),
content: SizedBox(
width: double.maxFinite,
content: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: screenSize.width * 0.8,
maxHeight: screenSize.height * 0.6,
),
child: Consumer(
builder: (context, ref, child) {
final playlistsAsync = ref
@@ -509,28 +674,32 @@ class LibraryScreen extends HookConsumerWidget {
final titleController = TextEditingController(text: track.title);
final artistController = TextEditingController(text: track.artist);
final albumController = TextEditingController(text: track.album);
final screenSize = MediaQuery.of(context).size;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Edit Track'),
content: Column(
spacing: 16,
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(labelText: 'Title'),
),
TextField(
controller: artistController,
decoration: const InputDecoration(labelText: 'Artist'),
),
TextField(
controller: albumController,
decoration: const InputDecoration(labelText: 'Album'),
),
],
content: ConstrainedBox(
constraints: BoxConstraints(maxWidth: screenSize.width * 0.8),
child: Column(
spacing: 16,
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(labelText: 'Title'),
),
TextField(
controller: artistController,
decoration: const InputDecoration(labelText: 'Artist'),
),
TextField(
controller: albumController,
decoration: const InputDecoration(labelText: 'Album'),
),
],
),
),
actions: [
TextButton(
@@ -570,13 +739,17 @@ class LibraryScreen extends HookConsumerWidget {
List<int> trackIds,
VoidCallback onSuccess,
) {
final screenSize = MediaQuery.of(context).size;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Add to Playlist'),
content: SizedBox(
width: double.maxFinite,
content: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: screenSize.width * 0.8,
maxHeight: screenSize.height * 0.6,
),
child: Consumer(
builder: (context, ref, child) {
final playlistsAsync = ref

View File

@@ -1,68 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
"info": {
"version": 1,
"author": "xcode"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
"images": [
{
"size": "16x16",
"idiom": "mac",
"filename": "app_icon_16.png",
"scale": "1x"
},
{
"size": "16x16",
"idiom": "mac",
"filename": "app_icon_32.png",
"scale": "2x"
},
{
"size": "32x32",
"idiom": "mac",
"filename": "app_icon_32.png",
"scale": "1x"
},
{
"size": "32x32",
"idiom": "mac",
"filename": "app_icon_64.png",
"scale": "2x"
},
{
"size": "128x128",
"idiom": "mac",
"filename": "app_icon_128.png",
"scale": "1x"
},
{
"size": "128x128",
"idiom": "mac",
"filename": "app_icon_256.png",
"scale": "2x"
},
{
"size": "256x256",
"idiom": "mac",
"filename": "app_icon_256.png",
"scale": "1x"
},
{
"size": "256x256",
"idiom": "mac",
"filename": "app_icon_512.png",
"scale": "2x"
},
{
"size": "512x512",
"idiom": "mac",
"filename": "app_icon_512.png",
"scale": "1x"
},
{
"size": "512x512",
"idiom": "mac",
"filename": "app_icon_1024.png",
"scale": "2x"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -41,6 +41,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
archive:
dependency: transitive
description:
@@ -249,6 +257,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.7"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
@@ -398,6 +414,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0+1"
flutter_native_splash:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002"
url: "https://pub.dev"
source: hosted
version: "2.4.7"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -472,6 +496,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http:
dependency: "direct main"
description:
@@ -1101,6 +1133,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
url: "https://pub.dev"
source: hosted
version: "2.3.1"
universal_platform:
dependency: transitive
description:

View File

@@ -68,6 +68,7 @@ dev_dependencies:
riverpod_generator: ^3.0.3
drift_dev: ^2.30.0
flutter_launcher_icons: ^0.14.4
flutter_native_splash: ^2.4.7
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -113,27 +114,27 @@ flutter:
flutter_launcher_icons:
android: "launcher_icon"
ios: true
image_path: "assets/images/logo.png"
image_path: "assets/images/icon.jpg"
min_sdk_android: 21
image_path_ios_dark_transparent: "assets/images/logo-dark.png"
image_path_ios_dark_transparent: "assets/images/icon-dark.png"
remove_alpha_ios: true
desaturate_tinted_to_grayscale_ios: true
background_color_ios: "#ffffff"
web:
generate: true
image_path: "assets/images/logo-dark.png"
image_path: "assets/images/icon-dark.png"
background_color: "#ffffff"
theme_color: "#2eb0c7"
windows:
generate: true
image_path: "assets/images/logo-dark.png"
image_path: "assets/images/icon-dark.png"
icon_size: 256
macos:
generate: true
image_path: "assets/images/icon-padded.png"
flutter_native_splash:
image: "assets/images/icon.png"
image_dark: "assets/images/icon-dark.png"
image: "assets/images/icon.jpg"
image_dark: "assets/images/icon-dark.jpg"
color: "#2eb0c7"
color_dark: "#121212"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -3,8 +3,8 @@
"short_name": "groovybox",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"background_color": "#ffffff",
"theme_color": "#2eb0c7",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
@@ -32,4 +32,4 @@
"purpose": "maskable"
}
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB