mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
9f1623bb34
- Updated .gitmodules to include mobile app submodule - Added swingmusic_mobile directory with Flutter app - Mobile app will now be built in unified release workflow
890 lines
28 KiB
Dart
890 lines
28 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import '../../shared/providers/audio_provider.dart';
|
|
import '../../data/models/track_model.dart';
|
|
|
|
class FolderScreen extends StatefulWidget {
|
|
const FolderScreen({super.key});
|
|
|
|
@override
|
|
State<FolderScreen> createState() => _FolderScreenState();
|
|
}
|
|
|
|
class _FolderScreenState extends State<FolderScreen> {
|
|
List<FolderItem> _folderItems = [];
|
|
List<FolderItem> _currentPath = [];
|
|
bool _isLoading = false;
|
|
final TextEditingController _searchController = TextEditingController();
|
|
String _currentFolderId = 'root';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadFolderContents(_currentFolderId);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadFolderContents(String folderId) async {
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
|
|
// Simulate API call
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
setState(() {
|
|
_isLoading = false;
|
|
if (folderId == 'root') {
|
|
_currentPath = [];
|
|
_folderItems = [
|
|
FolderItem(
|
|
id: 'music',
|
|
name: 'Music',
|
|
type: FolderType.folder,
|
|
itemCount: 156,
|
|
path: '/music',
|
|
modifiedDate: DateTime.now().subtract(const Duration(days: 1)),
|
|
),
|
|
FolderItem(
|
|
id: 'downloads',
|
|
name: 'Downloads',
|
|
type: FolderType.folder,
|
|
itemCount: 42,
|
|
path: '/downloads',
|
|
modifiedDate: DateTime.now().subtract(const Duration(hours: 3)),
|
|
),
|
|
FolderItem(
|
|
id: 'rock',
|
|
name: 'Rock',
|
|
type: FolderType.folder,
|
|
itemCount: 89,
|
|
path: '/music/rock',
|
|
modifiedDate: DateTime.now().subtract(const Duration(days: 7)),
|
|
),
|
|
FolderItem(
|
|
id: 'pop',
|
|
name: 'Pop',
|
|
type: FolderType.folder,
|
|
itemCount: 67,
|
|
path: '/music/pop',
|
|
modifiedDate: DateTime.now().subtract(const Duration(days: 2)),
|
|
),
|
|
FolderItem(
|
|
id: 'jazz',
|
|
name: 'Jazz',
|
|
type: FolderType.folder,
|
|
itemCount: 34,
|
|
path: '/music/jazz',
|
|
modifiedDate: DateTime.now().subtract(const Duration(days: 14)),
|
|
),
|
|
FolderItem(
|
|
id: 'electronic',
|
|
name: 'Electronic',
|
|
type: FolderType.folder,
|
|
itemCount: 78,
|
|
path: '/music/electronic',
|
|
modifiedDate: DateTime.now().subtract(const Duration(days: 5)),
|
|
),
|
|
FolderItem(
|
|
id: 'classical',
|
|
name: 'Classical',
|
|
type: FolderType.folder,
|
|
itemCount: 45,
|
|
path: '/music/classical',
|
|
modifiedDate: DateTime.now().subtract(const Duration(days: 21)),
|
|
),
|
|
];
|
|
} else if (folderId == 'music') {
|
|
_currentPath = [
|
|
FolderItem(id: 'root', name: '..', type: FolderType.folder, path: '/'),
|
|
FolderItem(id: 'rock', name: 'Rock', type: FolderType.folder, itemCount: 89, path: '/music/rock'),
|
|
FolderItem(id: 'pop', name: 'Pop', type: FolderType.folder, itemCount: 67, path: '/music/pop'),
|
|
FolderItem(id: 'jazz', name: 'Jazz', type: FolderType.folder, itemCount: 34, path: '/music/jazz'),
|
|
FolderItem(id: 'electronic', name: 'Electronic', type: FolderType.folder, itemCount: 78, path: '/music/electronic'),
|
|
FolderItem(id: 'classical', name: 'Classical', type: FolderType.folder, itemCount: 45, path: '/music/classical'),
|
|
];
|
|
_folderItems = [
|
|
TrackFolderItem(
|
|
id: '1',
|
|
name: 'Bohemian Rhapsody.mp3',
|
|
artist: 'Queen',
|
|
album: 'A Night at the Opera',
|
|
duration: 334,
|
|
size: 5.2,
|
|
modifiedDate: DateTime.now().subtract(const Duration(days: 30)),
|
|
),
|
|
TrackFolderItem(
|
|
id: '2',
|
|
name: 'Stairway to Heaven.mp3',
|
|
artist: 'Led Zeppelin',
|
|
album: 'Led Zeppelin IV',
|
|
duration: 482,
|
|
size: 7.8,
|
|
modifiedDate: DateTime.now().subtract(const Duration(days: 25)),
|
|
),
|
|
TrackFolderItem(
|
|
id: '3',
|
|
name: 'Hotel California.mp3',
|
|
artist: 'Eagles',
|
|
album: 'Hotel California',
|
|
duration: 391,
|
|
size: 6.1,
|
|
modifiedDate: DateTime.now().subtract(const Duration(days: 20)),
|
|
),
|
|
TrackFolderItem(
|
|
id: '4',
|
|
name: 'Sweet Child O\' Mine.mp3',
|
|
artist: 'Queen',
|
|
album: 'A Night at the Opera',
|
|
duration: 348,
|
|
size: 5.4,
|
|
modifiedDate: DateTime.now().subtract(const Duration(days: 15)),
|
|
),
|
|
TrackFolderItem(
|
|
id: '5',
|
|
name: 'Another Brick in the Wall.mp3',
|
|
artist: 'Pink Floyd',
|
|
album: 'The Wall',
|
|
duration: 623,
|
|
size: 9.8,
|
|
modifiedDate: DateTime.now().subtract(const Duration(days: 10)),
|
|
),
|
|
];
|
|
} else {
|
|
// Handle other folders with sample data
|
|
_currentPath = [
|
|
FolderItem(id: 'root', name: '..', type: FolderType.folder, path: '/'),
|
|
];
|
|
_folderItems = [
|
|
TrackFolderItem(
|
|
id: '1',
|
|
name: 'Sample Track.mp3',
|
|
artist: 'Sample Artist',
|
|
album: 'Sample Album',
|
|
duration: 240,
|
|
size: 4.2,
|
|
modifiedDate: DateTime.now().subtract(const Duration(days: 5)),
|
|
),
|
|
];
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(_getFolderTitle()),
|
|
elevation: 0,
|
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
leading: _currentFolderId != 'root'
|
|
? IconButton(
|
|
onPressed: () => _navigateToFolder('root'),
|
|
icon: const Icon(Icons.arrow_back),
|
|
)
|
|
: null,
|
|
actions: [
|
|
PopupMenuButton<String>(
|
|
onSelected: _handleMenuAction,
|
|
itemBuilder: (context) => [
|
|
const PopupMenuItem(
|
|
value: 'sort_name',
|
|
child: ListTile(
|
|
leading: const Icon(Icons.sort_by_alpha),
|
|
title: Text('Sort by Name'),
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
const PopupMenuItem(
|
|
value: 'sort_date',
|
|
child: ListTile(
|
|
leading: const Icon(Icons.access_time),
|
|
title: Text('Sort by Date'),
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
const PopupMenuItem(
|
|
value: 'sort_size',
|
|
child: ListTile(
|
|
leading: const Icon(Icons.storage),
|
|
title: Text('Sort by Size'),
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
const PopupMenuItem(
|
|
value: 'view_list',
|
|
child: ListTile(
|
|
leading: const Icon(Icons.list),
|
|
title: Text('List View'),
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
const PopupMenuItem(
|
|
value: 'view_grid',
|
|
child: ListTile(
|
|
leading: const Icon(Icons.grid_view),
|
|
title: Text('Grid View'),
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
// Breadcrumb Navigation
|
|
_buildBreadcrumbNavigation(),
|
|
|
|
// Search Bar
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: TextField(
|
|
controller: _searchController,
|
|
decoration: InputDecoration(
|
|
hintText: 'Search in current folder...',
|
|
prefixIcon: const Icon(Icons.search),
|
|
suffixIcon: _searchController.text.isNotEmpty
|
|
? IconButton(
|
|
onPressed: () {
|
|
_searchController.clear();
|
|
},
|
|
icon: const Icon(Icons.clear),
|
|
)
|
|
: null,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
filled: true,
|
|
fillColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
onChanged: (value) {
|
|
_filterItems(value);
|
|
},
|
|
),
|
|
),
|
|
|
|
// Folder Contents
|
|
Expanded(
|
|
child: _isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: _folderItems.isEmpty
|
|
? _buildEmptyState()
|
|
: _buildFolderContents(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBreadcrumbNavigation() {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: [
|
|
// Root/Home
|
|
InkWell(
|
|
onTap: () => _navigateToFolder('root'),
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: _currentFolderId == 'root'
|
|
? Theme.of(context).colorScheme.primaryContainer
|
|
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.home,
|
|
size: 20,
|
|
color: _currentFolderId == 'root'
|
|
? Theme.of(context).colorScheme.onPrimaryContainer
|
|
: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Home',
|
|
style: TextStyle(
|
|
color: _currentFolderId == 'root'
|
|
? Theme.of(context).colorScheme.onPrimaryContainer
|
|
: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Breadcrumb items
|
|
..._currentPath.map((item) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: 4),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.chevron_right, size: 16),
|
|
const SizedBox(width: 4),
|
|
InkWell(
|
|
onTap: () => _navigateToFolder(item.id),
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: item.id == _currentFolderId
|
|
? Theme.of(context).colorScheme.primaryContainer
|
|
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
item.name,
|
|
style: TextStyle(
|
|
color: item.id == _currentFolderId
|
|
? Theme.of(context).colorScheme.onPrimaryContainer
|
|
: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFolderContents() {
|
|
return ListView.builder(
|
|
padding: const EdgeInsets.all(8.0),
|
|
itemCount: _folderItems.length,
|
|
itemBuilder: (context, index) {
|
|
final item = _folderItems[index];
|
|
|
|
if (item is TrackFolderItem) {
|
|
return TrackFolderTile(
|
|
track: item,
|
|
onTap: () => _playTrack(item),
|
|
onPlay: () => _playTrack(item),
|
|
);
|
|
} else {
|
|
return FolderTile(
|
|
folder: item,
|
|
onTap: () => _navigateToFolder(item.id),
|
|
onLongPress: () => _showFolderOptions(item),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyState() {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.folder_open,
|
|
size: 64,
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'This folder is empty',
|
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Add music files to see them here',
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _getFolderTitle() {
|
|
if (_currentFolderId == 'root') {
|
|
return 'Music Library';
|
|
} else {
|
|
final folder = _currentPath.firstWhere(
|
|
(item) => item.id == _currentFolderId,
|
|
orElse: () => FolderItem(id: '', name: '', type: FolderType.folder),
|
|
);
|
|
return folder.name;
|
|
}
|
|
}
|
|
|
|
void _filterItems(String query) {
|
|
setState(() {
|
|
if (query.isEmpty) {
|
|
_loadFolderContents(_currentFolderId);
|
|
} else {
|
|
// Filter items (in real app, this would call API)
|
|
_folderItems = _folderItems.where((item) {
|
|
final name = item.name.toLowerCase();
|
|
return name.contains(query.toLowerCase());
|
|
}).toList();
|
|
}
|
|
});
|
|
}
|
|
|
|
void _navigateToFolder(String folderId) {
|
|
setState(() {
|
|
_currentFolderId = folderId;
|
|
});
|
|
_loadFolderContents(folderId);
|
|
}
|
|
|
|
void _playTrack(TrackFolderItem track) {
|
|
final trackModel = TrackModel(
|
|
id: int.parse(track.id),
|
|
title: track.name,
|
|
album: track.album,
|
|
albumhash: track.album.hashCode.toString(),
|
|
artists: [],
|
|
albumartists: [],
|
|
artisthashes: [],
|
|
track: 0,
|
|
disc: 1,
|
|
duration: track.duration,
|
|
bitrate: 320,
|
|
filepath: track.id, // Use ID as filepath for demo
|
|
folder: '',
|
|
genres: [],
|
|
genrehashes: [],
|
|
copyright: '',
|
|
date: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
|
lastModified: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
|
trackhash: track.id,
|
|
image: '',
|
|
weakHash: '',
|
|
extra: {},
|
|
lastplayed: 0,
|
|
playcount: 0,
|
|
playduration: track.duration,
|
|
explicit: false,
|
|
favUserids: [],
|
|
isFavorite: false,
|
|
score: 0.0,
|
|
);
|
|
|
|
final audioProvider = Provider.of<AudioProvider>(context, listen: false);
|
|
audioProvider.setQueue([trackModel]);
|
|
audioProvider.loadTrack(trackModel);
|
|
audioProvider.play();
|
|
|
|
Navigator.pushNamed(context, '/player');
|
|
}
|
|
|
|
void _handleMenuAction(String action) {
|
|
switch (action) {
|
|
case 'sort_name':
|
|
setState(() {
|
|
_folderItems.sort((a, b) => a.name.compareTo(b.name));
|
|
});
|
|
break;
|
|
case 'sort_date':
|
|
setState(() {
|
|
_folderItems.sort((a, b) => b.modifiedDate!.compareTo(a.modifiedDate!));
|
|
});
|
|
break;
|
|
case 'sort_size':
|
|
setState(() {
|
|
_folderItems.sort((a, b) {
|
|
final sizeA = a is TrackFolderItem ? a.size : 0;
|
|
final sizeB = b is TrackFolderItem ? b.size : 0;
|
|
return sizeA.compareTo(sizeB);
|
|
});
|
|
});
|
|
break;
|
|
// View options would be handled here
|
|
}
|
|
}
|
|
|
|
void _showFolderOptions(FolderItem folder) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
builder: (context) => Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ListTile(
|
|
leading: const Icon(Icons.play_arrow),
|
|
title: const Text('Play All'),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_playAllInFolder();
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.shuffle),
|
|
title: const Text('Shuffle All'),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_shuffleAllInFolder();
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.playlist_add),
|
|
title: const Text('Add to Playlist'),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_addToPlaylist();
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
ListTile(
|
|
leading: Icon(
|
|
Icons.delete,
|
|
color: Theme.of(context).colorScheme.error,
|
|
),
|
|
title: Text(
|
|
'Delete',
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.error,
|
|
),
|
|
),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_deleteFolder();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _playAllInFolder() {
|
|
// Implementation to play all tracks in folder
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Playing all tracks in folder')),
|
|
);
|
|
}
|
|
|
|
void _shuffleAllInFolder() {
|
|
// Implementation to shuffle all tracks in folder
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Shuffling all tracks in folder')),
|
|
);
|
|
}
|
|
|
|
void _addToPlaylist() {
|
|
// Implementation to add folder contents to playlist
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Added to playlist')),
|
|
);
|
|
}
|
|
|
|
void _deleteFolder() {
|
|
// Implementation to delete folder
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Folder deleted')),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Helper classes
|
|
class FolderItem {
|
|
final String id;
|
|
final String name;
|
|
final FolderType type;
|
|
final int? itemCount;
|
|
final String? path;
|
|
final DateTime? modifiedDate;
|
|
|
|
FolderItem({
|
|
required this.id,
|
|
required this.name,
|
|
required this.type,
|
|
this.itemCount,
|
|
this.path,
|
|
this.modifiedDate,
|
|
});
|
|
}
|
|
|
|
class TrackFolderItem extends FolderItem {
|
|
final String artist;
|
|
final String album;
|
|
final int duration;
|
|
final double size;
|
|
|
|
TrackFolderItem({
|
|
required String id,
|
|
required String name,
|
|
required this.artist,
|
|
required this.album,
|
|
required this.duration,
|
|
required this.size,
|
|
String? path,
|
|
DateTime? modifiedDate,
|
|
}) : super(
|
|
id: id,
|
|
name: name,
|
|
type: FolderType.track,
|
|
path: path,
|
|
modifiedDate: modifiedDate ?? DateTime.now(),
|
|
);
|
|
}
|
|
|
|
enum FolderType {
|
|
folder,
|
|
track,
|
|
}
|
|
|
|
// Custom widgets
|
|
class FolderTile extends StatelessWidget {
|
|
final FolderItem folder;
|
|
final VoidCallback onTap;
|
|
final VoidCallback? onLongPress;
|
|
|
|
const FolderTile({
|
|
super.key,
|
|
required this.folder,
|
|
required this.onTap,
|
|
this.onLongPress,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
onLongPress: onLongPress,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
children: [
|
|
// Folder/Track Icon
|
|
Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: folder.type == FolderType.folder
|
|
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
|
|
: Theme.of(context).colorScheme.secondary.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8.0),
|
|
),
|
|
child: Icon(
|
|
folder.type == FolderType.folder ? Icons.folder : Icons.music_note,
|
|
color: folder.type == FolderType.folder
|
|
? Theme.of(context).colorScheme.primary
|
|
: Theme.of(context).colorScheme.secondary,
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
// Folder/Track Info
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
folder.name,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 4),
|
|
if (folder.itemCount != null)
|
|
Text(
|
|
'${folder.itemCount} items',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
if (folder.modifiedDate != null)
|
|
Text(
|
|
_formatDate(folder.modifiedDate!),
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
if (folder is TrackFolderItem)
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
(folder as TrackFolderItem).artist,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
Text(
|
|
(folder as TrackFolderItem).album,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class TrackFolderTile extends StatelessWidget {
|
|
final TrackFolderItem track;
|
|
final VoidCallback onTap;
|
|
final VoidCallback? onPlay;
|
|
|
|
const TrackFolderTile({
|
|
super.key,
|
|
required this.track,
|
|
required this.onTap,
|
|
this.onPlay,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
children: [
|
|
// Track Icon
|
|
Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.secondary.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
Icons.music_note,
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
// Track Info
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
track.name,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
track.artist,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
Text(
|
|
track.album,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Text(
|
|
_formatDuration(track.duration),
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'${track.size.toStringAsFixed(1)} MB',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Play Button
|
|
IconButton(
|
|
onPressed: onPlay,
|
|
icon: const Icon(Icons.play_arrow),
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
String _formatDate(DateTime date) {
|
|
final now = DateTime.now();
|
|
final difference = now.difference(date);
|
|
|
|
if (difference.inDays == 0) {
|
|
return 'Today';
|
|
} else if (difference.inDays == 1) {
|
|
return 'Yesterday';
|
|
} else if (difference.inDays < 7) {
|
|
return '${difference.inDays} days ago';
|
|
} else if (difference.inDays < 30) {
|
|
return '${(difference.inDays / 7).floor()} weeks ago';
|
|
} else {
|
|
return '${(difference.inDays / 30).floor()} months ago';
|
|
}
|
|
}
|
|
|
|
String _formatDuration(int seconds) {
|
|
final minutes = seconds ~/ 60;
|
|
final remainingSeconds = seconds % 60;
|
|
return '${minutes}:${remainingSeconds.toString().padLeft(2, '0')}';
|
|
}
|