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’s playback system is built around SongPlayedScreen — a full-screen Now Playing view that slides up over the home screen — backed by AudioService and AudioPlayerHandler for persistent background playback. This page covers every control available to the listener, the queue view, and the technical plumbing that ties them together.

Starting playback

All playback entry points funnel through MusicOrchestratorService, which resolves the correct song list for the chosen context, loads it into just_audio, and starts the player.
// Signature of the primary playback entry point
void playSong(
  SongModel song,
  PlaylistType type, {
  required String heroId,
  int? id,
  bool activateShuffle = false,
})
ParameterPurpose
songThe track to play first
typeWhich PlaylistType to resolve as the queue (e.g. PlaylistType.album)
heroIdFlutter Hero tag so the artwork animates smoothly from list to Now Playing
idCollection ID (album/artist/genre/playlist) when type is not songs or favorites
activateShuffleWhen true, enables shuffle immediately (used by the FAB shuffle action)
On a cold start, MusicOrchestratorService.initSong(song, {heroId}) restores the last-played track and seeks to the saved position without auto-playing.

Now Playing screen (SongPlayedScreen)

The screen is pushed via a slide-up route transition and uses opaque: false so the home screen remains visible through the closing slide animation (avoiding a black flash). The scaffold background is Colors.transparent; the visual backdrop is produced by blurring and tinting the album artwork using ImageFiltered.

Dynamic theming

When a song starts, the dominant colour is extracted natively from the artwork (via queryArtworkColorandroidx.palette on Android, Core Image on iOS). That colour propagates through the local Theme to typography, icons, and the progress bar, so the Now Playing screen always feels visually cohesive with the current artwork.
Theme(
  data: AppTheme.darkTheme.copyWith(
    textTheme: uiState.songPlayedTypography,
    colorScheme: ColorScheme.dark(
      primary: songPlayedThemeColor,
      onSurface: songPlayedThemeColor,
    ),
    iconButtonTheme: IconButtonThemeData(
      style: ButtonStyle(
        iconColor: WidgetStatePropertyAll(songPlayedThemeColor),
      ),
    ),
  ),
  child: /* screen body */,
)

Orientations

The screen has fully adapted portrait and landscape layouts. In landscape mode the artwork sits in a fixed-width column on the left; controls and metadata fill the right panel.
The artwork area is a PageView over the current queue (PlaybackStateCubit.currentPlaylist). Each page shows the album art for one track.
  • Swipe left/right to skip to the adjacent song. The swipe calls AudioPlayer.seek(Duration.zero, index: newIndex) and resumes playback.
  • External song changes (skip buttons, auto-advance) animate the carousel to the correct page via animateToPage, keeping the two in sync without fighting an in-progress user drag.
  • Volume via vertical drag — dragging up/down on the artwork adjusts system volume through the volume_controller package. A translucent pill overlay shows the current level and icon.
A stale-result race condition is avoided by tracking a _userSwipe latch: onPageChanged only seeks when the page change originated from a real user drag (ScrollStartNotification.dragDetails != null). Programmatic animateToPage calls never trigger a seek.

Playback controls

ControlBehaviour
Play / PauseAn AnimatedIcon (play ↔ pause) with a 200 ms AnimationController. Streams AudioPlayer.playingStream to stay in sync with the player state.
Skip next / previousTap = clean skip to the next or previous track. Long-press = progressive ±10 s scrub (500 ms delay before the first seek, then every 500 ms while held).
ShuffleToggles AudioPlayer.setShuffleModeEnabled. The icon opacity indicates state: full-opacity = on, dimmed = off.
Repeat / LoopCycles through LoopMode.offLoopMode.oneLoopMode.all with matching icons (Icons.repeat, Icons.repeat_one, Icons.keyboard_double_arrow_right). A toast confirms the new mode.

Skip long-press scrub

The press-and-hold scrub is implemented with a Listener widget that polls every 500 ms while the pointer is down, reading the live player position and seeking by ±10 s. A 500 ms initial delay ensures a quick tap always fires a clean skip.
void _whilePressed({
  required AudioPlayer audioPlayer,
  required int currentIndex,
  required int songDurationSeconds,
  required int goToSeconds,
}) async {
  if (_loopActive) return;
  _loopActive = true;

  // Wait before the first seek so a quick tap (skip to next/previous song)
  // doesn't trigger a scrub jump. Scrubbing only starts while held.
  await Future.delayed(const Duration(milliseconds: 500));

  while (_buttonPressed) {
    if (currentIndex != audioPlayer.currentIndex) {
      _buttonPressed = false;
      break;
    }

    // Read the live position so holding scrubs progressively.
    final secondsResult = audioPlayer.position.inSeconds + goToSeconds;

    if (secondsResult <= 0 || secondsResult >= songDurationSeconds) {
      _buttonPressed = false;
    }

    audioPlayer.seek(
      Duration(seconds: secondsResult.clamp(0, songDurationSeconds)),
    );
    await Future.delayed(const Duration(milliseconds: 500));
  }

  _loopActive = false;
}

Progress bar and seek scrubbing

The _SongTimeline widget wraps the audio_video_progress_bar package’s ProgressBar, displaying elapsed and total time with a draggable thumb.
ProgressBar(
  thumbGlowRadius: 16.0,
  thumbRadius: 8.0,
  barHeight: 3.0,
  progress: audioControlState.currentDuration,
  total: Duration(milliseconds: songPlayed.duration.nonNullValue()),
  onSeek: audioPlayer.seek,
)
Dragging the thumb calls AudioPlayer.seek directly, jumping playback to the chosen position with no additional debouncing.

Favourite button

A Icons.favorite / Icons.favorite_border button in the Now Playing screen calls FavoritesService.toggle(song, ...). The same toggle is also available from:
  • The MoreSongOptionsModal (song context menu, accessible from any list).
  • The persistent media notification (custom favorite action).
The icon responds immediately because FavoritesCubit is a Bloc whose state is watched directly:
final isFavoriteSong = context.select(
  (FavoritesCubit c) => c.state.isFavoriteSong(songPlayed.id),
);

Playing queue (PlayingQueueScreen)

Tap the queue icon (top-right of the Now Playing screen) to open the full queue view.
1

Queue header

Shows “Up next • [position]/[total] • [total duration]” beneath the app bar.
2

Drag to reorder

Each row has a ReorderableDelayedDragStartListener drag handle. Releasing calls PlaybackService.moveInQueue(oldIndex, newIndex) to update the live player order.
3

Remove from queue

Tap the Icons.remove_circle_outline button on a row. Calls PlaybackService.removeFromQueue(song).
4

Jump to current track

The queue scrolls to the currently-playing song on open. Tapping the mini-player bar at the bottom re-triggers the same scroll if you’ve moved elsewhere.
The queue also exposes shuffle and loop-mode toggles in its app bar so you don’t have to navigate back to the Now Playing screen.
A song can appear in the queue more than once (via Play next or Add to queue). Each tile uses a per-position key 'queue-$actualIndex-${song.id}' to prevent duplicate Hero tag collisions.

Background playback and media notification

Musynth uses the audio_service package with a custom AudioPlayerHandler. Once a song starts, a persistent system notification appears with:
ActionButton
Play / Pause▶ / ⏸
Skip previous
Skip next
Toggle favourite♥ (custom action)
Dismiss / Stop
Audio continues playing when the app is backgrounded, the screen locks, or the app is removed from the recent-apps drawer (subject to device battery-optimisation settings).

Mini-player bar (CurrentSongTile)

A compact now-playing strip is docked to the bottom of every main screen while a song is loaded. It shows the artwork, title, and a play/pause button. Tapping it navigates to the full SongPlayedScreen.

Build docs developers (and LLMs) love