🍱 Regenerate app icons
@@ -7,7 +7,7 @@
|
|||||||
<application
|
<application
|
||||||
android:label="groovybox"
|
android:label="groovybox"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/launcher_icon">
|
||||||
<activity android:name="com.ryanheise.audioservice.AudioServiceActivity"></activity>
|
<activity android:name="com.ryanheise.audioservice.AudioServiceActivity"></activity>
|
||||||
|
|
||||||
<service android:name="com.ryanheise.audioservice.AudioService"
|
<service android:name="com.ryanheise.audioservice.AudioService"
|
||||||
|
|||||||
BIN
android/app/src/main/res/mipmap-hdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 830 B |
BIN
android/app/src/main/res/mipmap-mdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 550 B |
BIN
android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -429,7 +429,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
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_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
@@ -486,7 +486,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
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_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|||||||
@@ -1,122 +1 @@
|
|||||||
{
|
{"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"}}
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 474 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 685 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 723 B |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 855 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 910 B |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 524 B |
|
After Width: | Height: | Size: 728 B |
|
After Width: | Height: | Size: 753 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 992 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -48,6 +48,8 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
final selectedTrackIds = useState<Set<int>>({});
|
final selectedTrackIds = useState<Set<int>>({});
|
||||||
final searchQuery = useState<String>('');
|
final searchQuery = useState<String>('');
|
||||||
final isSelectionMode = selectedTrackIds.value.isNotEmpty;
|
final isSelectionMode = selectedTrackIds.value.isNotEmpty;
|
||||||
|
final isLargeScreen = MediaQuery.of(context).size.width > 600;
|
||||||
|
final selectedTab = isLargeScreen ? useState<int>(0) : null;
|
||||||
|
|
||||||
void toggleSelection(int id) {
|
void toggleSelection(int id) {
|
||||||
final newSet = Set<int>.from(selectedTrackIds.value);
|
final newSet = Set<int>.from(selectedTrackIds.value);
|
||||||
@@ -63,9 +65,8 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
selectedTrackIds.value = {};
|
selectedTrackIds.value = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return DefaultTabController(
|
if (isLargeScreen) {
|
||||||
length: 3,
|
return Scaffold(
|
||||||
child: Scaffold(
|
|
||||||
appBar: isSelectionMode
|
appBar: isSelectionMode
|
||||||
? AppBar(
|
? AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@@ -105,13 +106,6 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
: AppBar(
|
: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: const Text('Library'),
|
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: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.add_circle_outline),
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
@@ -164,224 +158,391 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: TabBarView(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
// Tracks Tab (Existing Logic)
|
NavigationRail(
|
||||||
StreamBuilder<List<Track>>(
|
selectedIndex: selectedTab!.value,
|
||||||
stream: repo.watchAllTracks(),
|
onDestinationSelected: (index) => selectedTab.value = index,
|
||||||
builder: (context, snapshot) {
|
destinations: const [
|
||||||
if (snapshot.hasError) {
|
NavigationRailDestination(
|
||||||
return Center(child: Text('Error: ${snapshot.error}'));
|
icon: Icon(Icons.audiotrack),
|
||||||
}
|
label: Text('Tracks'),
|
||||||
if (!snapshot.hasData) {
|
),
|
||||||
return const Center(child: CircularProgressIndicator());
|
NavigationRailDestination(
|
||||||
}
|
icon: Icon(Icons.album),
|
||||||
final tracks = snapshot.data!;
|
label: Text('Albums'),
|
||||||
if (tracks.isEmpty) {
|
),
|
||||||
return const Center(child: Text('No tracks yet. Add some!'));
|
NavigationRailDestination(
|
||||||
}
|
icon: Icon(Icons.queue_music),
|
||||||
|
label: Text('Playlists'),
|
||||||
List<Track> filteredTracks;
|
),
|
||||||
if (searchQuery.value.isEmpty) {
|
],
|
||||||
filteredTracks = tracks;
|
),
|
||||||
} else {
|
Expanded(
|
||||||
final query = searchQuery.value.toLowerCase();
|
child: _buildTabContent(
|
||||||
filteredTracks = tracks.where((track) {
|
selectedTab.value,
|
||||||
if (track.title.toLowerCase().contains(query)) return true;
|
ref,
|
||||||
if (track.artist?.toLowerCase().contains(query) ?? false)
|
repo,
|
||||||
return true;
|
selectedTrackIds,
|
||||||
if (track.album?.toLowerCase().contains(query) ?? false)
|
searchQuery,
|
||||||
return true;
|
toggleSelection,
|
||||||
if (track.lyrics != null) {
|
isSelectionMode,
|
||||||
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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// 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) {
|
void _showTrackOptions(BuildContext context, WidgetRef ref, Track track) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -439,6 +600,7 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
Track track,
|
Track track,
|
||||||
) {
|
) {
|
||||||
|
final screenSize = MediaQuery.of(context).size;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@@ -448,8 +610,11 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
// Or we can use a Consumer widget inside the dialog.
|
// Or we can use a Consumer widget inside the dialog.
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Add to Playlist'),
|
title: const Text('Add to Playlist'),
|
||||||
content: SizedBox(
|
content: ConstrainedBox(
|
||||||
width: double.maxFinite,
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: screenSize.width * 0.8,
|
||||||
|
maxHeight: screenSize.height * 0.6,
|
||||||
|
),
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final playlistsAsync = ref
|
final playlistsAsync = ref
|
||||||
@@ -509,28 +674,32 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
final titleController = TextEditingController(text: track.title);
|
final titleController = TextEditingController(text: track.title);
|
||||||
final artistController = TextEditingController(text: track.artist);
|
final artistController = TextEditingController(text: track.artist);
|
||||||
final albumController = TextEditingController(text: track.album);
|
final albumController = TextEditingController(text: track.album);
|
||||||
|
final screenSize = MediaQuery.of(context).size;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('Edit Track'),
|
title: const Text('Edit Track'),
|
||||||
content: Column(
|
content: ConstrainedBox(
|
||||||
spacing: 16,
|
constraints: BoxConstraints(maxWidth: screenSize.width * 0.8),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
children: [
|
spacing: 16,
|
||||||
TextField(
|
mainAxisSize: MainAxisSize.min,
|
||||||
controller: titleController,
|
children: [
|
||||||
decoration: const InputDecoration(labelText: 'Title'),
|
TextField(
|
||||||
),
|
controller: titleController,
|
||||||
TextField(
|
decoration: const InputDecoration(labelText: 'Title'),
|
||||||
controller: artistController,
|
),
|
||||||
decoration: const InputDecoration(labelText: 'Artist'),
|
TextField(
|
||||||
),
|
controller: artistController,
|
||||||
TextField(
|
decoration: const InputDecoration(labelText: 'Artist'),
|
||||||
controller: albumController,
|
),
|
||||||
decoration: const InputDecoration(labelText: 'Album'),
|
TextField(
|
||||||
),
|
controller: albumController,
|
||||||
],
|
decoration: const InputDecoration(labelText: 'Album'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -570,13 +739,17 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
List<int> trackIds,
|
List<int> trackIds,
|
||||||
VoidCallback onSuccess,
|
VoidCallback onSuccess,
|
||||||
) {
|
) {
|
||||||
|
final screenSize = MediaQuery.of(context).size;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Add to Playlist'),
|
title: const Text('Add to Playlist'),
|
||||||
content: SizedBox(
|
content: ConstrainedBox(
|
||||||
width: double.maxFinite,
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: screenSize.width * 0.8,
|
||||||
|
maxHeight: screenSize.height * 0.6,
|
||||||
|
),
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final playlistsAsync = ref
|
final playlistsAsync = ref
|
||||||
|
|||||||
@@ -1,68 +1,68 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"info": {
|
||||||
{
|
"version": 1,
|
||||||
"size" : "16x16",
|
"author": "xcode"
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "app_icon_16.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
},
|
||||||
{
|
"images": [
|
||||||
"size" : "16x16",
|
{
|
||||||
"idiom" : "mac",
|
"size": "16x16",
|
||||||
"filename" : "app_icon_32.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_16.png",
|
||||||
},
|
"scale": "1x"
|
||||||
{
|
},
|
||||||
"size" : "32x32",
|
{
|
||||||
"idiom" : "mac",
|
"size": "16x16",
|
||||||
"filename" : "app_icon_32.png",
|
"idiom": "mac",
|
||||||
"scale" : "1x"
|
"filename": "app_icon_32.png",
|
||||||
},
|
"scale": "2x"
|
||||||
{
|
},
|
||||||
"size" : "32x32",
|
{
|
||||||
"idiom" : "mac",
|
"size": "32x32",
|
||||||
"filename" : "app_icon_64.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_32.png",
|
||||||
},
|
"scale": "1x"
|
||||||
{
|
},
|
||||||
"size" : "128x128",
|
{
|
||||||
"idiom" : "mac",
|
"size": "32x32",
|
||||||
"filename" : "app_icon_128.png",
|
"idiom": "mac",
|
||||||
"scale" : "1x"
|
"filename": "app_icon_64.png",
|
||||||
},
|
"scale": "2x"
|
||||||
{
|
},
|
||||||
"size" : "128x128",
|
{
|
||||||
"idiom" : "mac",
|
"size": "128x128",
|
||||||
"filename" : "app_icon_256.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_128.png",
|
||||||
},
|
"scale": "1x"
|
||||||
{
|
},
|
||||||
"size" : "256x256",
|
{
|
||||||
"idiom" : "mac",
|
"size": "128x128",
|
||||||
"filename" : "app_icon_256.png",
|
"idiom": "mac",
|
||||||
"scale" : "1x"
|
"filename": "app_icon_256.png",
|
||||||
},
|
"scale": "2x"
|
||||||
{
|
},
|
||||||
"size" : "256x256",
|
{
|
||||||
"idiom" : "mac",
|
"size": "256x256",
|
||||||
"filename" : "app_icon_512.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_256.png",
|
||||||
},
|
"scale": "1x"
|
||||||
{
|
},
|
||||||
"size" : "512x512",
|
{
|
||||||
"idiom" : "mac",
|
"size": "256x256",
|
||||||
"filename" : "app_icon_512.png",
|
"idiom": "mac",
|
||||||
"scale" : "1x"
|
"filename": "app_icon_512.png",
|
||||||
},
|
"scale": "2x"
|
||||||
{
|
},
|
||||||
"size" : "512x512",
|
{
|
||||||
"idiom" : "mac",
|
"size": "512x512",
|
||||||
"filename" : "app_icon_1024.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_512.png",
|
||||||
}
|
"scale": "1x"
|
||||||
],
|
},
|
||||||
"info" : {
|
{
|
||||||
"version" : 1,
|
"size": "512x512",
|
||||||
"author" : "xcode"
|
"idiom": "mac",
|
||||||
}
|
"filename": "app_icon_1024.png",
|
||||||
}
|
"scale": "2x"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 512 B |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 978 B |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.0 KiB |
40
pubspec.lock
@@ -41,6 +41,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
ansicolor:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ansicolor
|
||||||
|
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -249,6 +257,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -398,6 +414,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0+1"
|
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:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -472,6 +496,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1101,6 +1133,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
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:
|
universal_platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
13
pubspec.yaml
@@ -68,6 +68,7 @@ dev_dependencies:
|
|||||||
riverpod_generator: ^3.0.3
|
riverpod_generator: ^3.0.3
|
||||||
drift_dev: ^2.30.0
|
drift_dev: ^2.30.0
|
||||||
flutter_launcher_icons: ^0.14.4
|
flutter_launcher_icons: ^0.14.4
|
||||||
|
flutter_native_splash: ^2.4.7
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
@@ -113,27 +114,27 @@ flutter:
|
|||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: "launcher_icon"
|
android: "launcher_icon"
|
||||||
ios: true
|
ios: true
|
||||||
image_path: "assets/images/logo.png"
|
image_path: "assets/images/icon.jpg"
|
||||||
min_sdk_android: 21
|
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
|
remove_alpha_ios: true
|
||||||
desaturate_tinted_to_grayscale_ios: true
|
desaturate_tinted_to_grayscale_ios: true
|
||||||
background_color_ios: "#ffffff"
|
background_color_ios: "#ffffff"
|
||||||
web:
|
web:
|
||||||
generate: true
|
generate: true
|
||||||
image_path: "assets/images/logo-dark.png"
|
image_path: "assets/images/icon-dark.png"
|
||||||
background_color: "#ffffff"
|
background_color: "#ffffff"
|
||||||
theme_color: "#2eb0c7"
|
theme_color: "#2eb0c7"
|
||||||
windows:
|
windows:
|
||||||
generate: true
|
generate: true
|
||||||
image_path: "assets/images/logo-dark.png"
|
image_path: "assets/images/icon-dark.png"
|
||||||
icon_size: 256
|
icon_size: 256
|
||||||
macos:
|
macos:
|
||||||
generate: true
|
generate: true
|
||||||
image_path: "assets/images/icon-padded.png"
|
image_path: "assets/images/icon-padded.png"
|
||||||
|
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
image: "assets/images/icon.png"
|
image: "assets/images/icon.jpg"
|
||||||
image_dark: "assets/images/icon-dark.png"
|
image_dark: "assets/images/icon-dark.jpg"
|
||||||
color: "#2eb0c7"
|
color: "#2eb0c7"
|
||||||
color_dark: "#121212"
|
color_dark: "#121212"
|
||||||
BIN
web/favicon.png
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 249 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 5.8 KiB |
@@ -3,8 +3,8 @@
|
|||||||
"short_name": "groovybox",
|
"short_name": "groovybox",
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#0175C2",
|
"background_color": "#ffffff",
|
||||||
"theme_color": "#0175C2",
|
"theme_color": "#2eb0c7",
|
||||||
"description": "A new Flutter project.",
|
"description": "A new Flutter project.",
|
||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"prefer_related_applications": false,
|
"prefer_related_applications": false,
|
||||||
@@ -32,4 +32,4 @@
|
|||||||
"purpose": "maskable"
|
"purpose": "maskable"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 2.9 KiB |