Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CesarArellano/Music-Player-App/llms.txt

Use this file to discover all available pages before exploring further.

Musynth scans your device’s local storage and surfaces every audio file across six purpose-built tabs — Songs, Albums, Artists, Playlist (Android only), Favourites, and Genres. This page explains what each tab shows, how fast-scroll and collection screens work, and how the PlaylistType enum ties every tab to a distinct playback context.

Tab overview

TabDescription
SongsAll tracks sorted alphabetically by title, with an A–Z fast-scroll rail on the right edge.
AlbumsA 2-column artwork grid (4 columns in landscape). Tap an album to open its track list.
ArtistsA scrollable list showing album and track counts. Tap to see an artist’s albums and songs.
PlaylistAndroid only. Create and delete MediaStore playlists, add songs from any context menu.
FavouritesPersisted favourites list. Plays as its own PlaylistType.favorites queue.
GenresEvery genre reported by the device. Tap to browse songs filtered to that genre.
The Playlist tab is conditionally rendered only on Android. The TabController length is set to 6 on Android and 5 on iOS, and all playlist-related operations are guarded by Platform.isAndroid.

Songs tab

The Songs tab (SongsScreen) is the primary view. When the library loads, songs are already sorted by title (SongSortType.TITLE) so the alphabet rail is immediately meaningful.

Alphabet fast-scroll rail

A compact AlphabetScrollbar widget sits on the right edge of the Songs tab. It:
  • Auto-reveals whenever the list is scrolling, then fades out after 1.5 s of inactivity.
  • Highlights the active letter with a floating pill bubble.
  • Triggers haptic feedback on each letter change via HapticFeedback.selectionClick().
  • Maps a touch Y-position to a letter index in O(1) time — each letter occupies a fixed letterExtent slice, so the index is simply dy ~/ letterExtent.
// AlphabetScrollbar touch-to-index calculation (O(1))
void _select(double dy) {
  final index =
      (dy ~/ widget.letterExtent).clamp(0, widget.letters.length - 1);
  if (index != _activeIndex.value) {
    _activeIndex.value = index;
    widget.onLetterSelected(widget.letters[index]);
    HapticFeedback.selectionClick();
  }
}
The Songs screen memoises the letter-to-index map in _buildIndex(), only recomputing when the song list reference changes:
void _buildIndex(List<SongModel> songList) {
  if (identical(_indexedFor, songList)) return;
  _indexedFor = songList;

  final letters = <String>[];
  final letterToIndex = <String, int>{};
  for (int i = 0; i < songList.length; i++) {
    final letter = _firstLetter(songList[i].title.nonNullValue());
    if (!letterToIndex.containsKey(letter)) {
      letterToIndex[letter] = i;
      letters.add(letter);
    }
  }
  _letters = letters;
  _letterToIndex = letterToIndex;
}
Tapping a letter computes the pixel offset from the header height and row extent, then calls ScrollController.jumpTo() for an instant, jank-free jump.
Accented characters (á, é, ñ, ç, etc.) are folded to their base Latin letter before indexing, so “Ángel” appears under A and “Ñoño” appears under N.

Scroll-to-top button

A floating scroll-to-top button appears once the user scrolls down more than 50 px and is actively scrolling upward. It animates the list back to position 0 in 300 ms.

Albums tab

AlbumsScreen renders a GridView with:
  • 2 columns in portrait, 4 columns in landscape.
  • Each card shows the album artwork (174 px tall), the album title, and the song count.
  • Tapping navigates to AlbumSelectedScreen via AppRoutes.album.

Artists tab

ArtistScreen renders a list where each row shows the artist artwork, name, album count, and track count. Tapping navigates to ArtistSelectedScreen, which calls LibraryCubit.searchByArtistId to load songs and albums for that artist.

Off-thread artist resolution

searchByArtistId runs its filter and aggregation in a Dart Isolate, keeping the UI thread free while O(k+m) work completes (k = songs, m = albums):
Future<void> searchByArtistId(int artistId, {bool force = false}) async {
  final allSongs = state.songList;
  final allAlbums = state.albumList;

  final result = await Isolate.run(() {
    final artistSongs =
        allSongs.where((s) => s.artistId == artistId).toList();
    final albumIds = <int>{};
    int totalMs = 0;
    for (final song in artistSongs) {
      totalMs += song.duration ?? 0;
      if ((song.albumId ?? 0) != 0) albumIds.add(song.albumId!);
    }
    final albumById = {for (final a in allAlbums) a.id: a};
    final albums = albumIds
        .map((id) => albumById[id])
        .whereType<AlbumModel>()
        .toList();
    artistSongs.sort((a, b) => a.id.compareTo(b.id));
    return (songs: artistSongs, albums: albums, totalMs: totalMs);
  });
  // result cached in state.artistCollection[artistId]
}
Results are cached in LibraryState.artistCollection keyed by artistId, so subsequent visits are instant.

Playlists tab (Android only)

PlaylistsScreen lists all MediaStore playlists. Each row shows the playlist artwork, name, and song count, with a delete icon on the trailing edge.

Creating a playlist

Tap the + FAB (visible only when the Playlists tab is active) to open CreatePlaylistDialog. Confirming calls MusicQuerySelector.createPlaylist(name) and then refreshes the list:
final onAudioQuery = setupLocator<MusicQuerySelector>();
await onAudioQuery.createPlaylist(playlistName);
libraryCubit.refreshPlaylist();

Adding songs to a playlist

Long-press any song → MoreSongOptionsModalAdd to playlist → choose from a dropdown.

Deleting a playlist

Tap the delete icon or long-press a playlist row. The modal calls MusicQuerySelector.removePlaylist(id), then LibraryCubit.refreshPlaylist().

Favourites tab

FavoriteScreen reads from FavoritesCubit.state.favoriteList — a decoded list of SongModel instances persisted via SharedPreferences. Tapping a song plays the entire favourites list as PlaylistType.favorites. See Favourites & Playlists for the full persistence and toggle details.

Genres tab

GenresScreen renders a list of every genre. Each row shows the genre artwork, name, and song count. Tapping navigates to GenreSelectedScreen, which calls LibraryCubit.searchByGenreId(genreId) to load the filtered song list.

Collection screens

AlbumSelectedScreen, ArtistSelectedScreen, GenreSelectedScreen, and PlaylistSelectedScreen share a common layout:
1

Collapsing header

A large artwork and metadata block at the top. As you scroll past 191 px the app-bar title fades in and the header collapses.
2

Sticky Play / Shuffle row

A PlayShuffleButtons widget pinned to the top of the song list. Play starts the collection from the first track; Shuffle randomises the order.
3

Song list

Album view shows track numbers; artist view shows albums in a horizontal scroll carousel above the song list.

PlaylistType enum

Every tab and collection screen resolves its song list through a PlaylistType value. This enum is the single source of truth that MusicOrchestratorService.playSong and PlaylistResolverFactory use to build the correct queue.
ValueSource
PlaylistType.songsFull LibraryState.songList
PlaylistType.albumLibraryState.albumCollection[id]
PlaylistType.artistLibraryState.artistCollection[id].songs
PlaylistType.genreLibraryState.genreCollection[id]
PlaylistType.playlistLibraryState.playlistCollection[id]
PlaylistType.favoritesFavoritesCubit.state.favoriteList

Floating action button behaviour

The FAB adapts based on the active tab index:
Active tabFAB iconAction
Any except PlaylistIcons.shufflePicks a random song and starts the full PlaylistType.songs queue with shuffle enabled
Playlist (Android)Icons.addOpens CreatePlaylistDialog
The FAB is hidden entirely when the song list is empty. While artwork caching is in progress (isCreatingArtworks: true), the FAB remains visible but its onPressed is set to null (disabled) and its child is replaced with a CircularProgressIndicator.

Scanning for new media

Open the app-bar overflow menu (⋮) and tap Scan media. This calls LibraryCubit.getAllSongs(), which re-queries all four MediaStore collections (songs, albums, artists, genres — and playlists on Android) concurrently and rebuilds the artwork cache for any new artwork.
PopupMenuItem(
  child: const Text('Scan media'),
  onTap: () async {
    await libraryCubit.getAllSongs();
    SnackbarService.instance
        .showSnackbar(message: 'Task successfully completed');
  },
),

Build docs developers (and LLMs) love