Skip to content

Commit

Permalink
feat: interactive music player (#6)
Browse files Browse the repository at this point in the history
* feat: MusicRepository

* feat: interactive music player

* fix: tracks count

* test: fixed failing
  • Loading branch information
jneschisi authored Jul 29, 2024
1 parent 712b68d commit f3dc8f5
Show file tree
Hide file tree
Showing 25 changed files with 1,553 additions and 162 deletions.
1 change: 1 addition & 0 deletions ios/Flutter/Debug.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
1 change: 1 addition & 0 deletions ios/Flutter/Release.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
44 changes: 44 additions & 0 deletions ios/Podfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}

def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end

File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
use_frameworks!
use_modular_headers!

flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end

post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
35 changes: 35 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
PODS:
- audio_session (0.0.1):
- Flutter
- Flutter (1.0.0)
- just_audio (0.0.1):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS

DEPENDENCIES:
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- Flutter (from `Flutter`)
- just_audio (from `.symlinks/plugins/just_audio/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)

EXTERNAL SOURCES:
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
Flutter:
:path: Flutter
just_audio:
:path: ".symlinks/plugins/just_audio/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"

SPEC CHECKSUMS:
audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46

PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796

COCOAPODS: 1.15.2
198 changes: 170 additions & 28 deletions ios/Runner.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions ios/Runner.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 19 additions & 6 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,31 @@ import 'package:aes_ui/aes_ui.dart';
import 'package:airplane_entertainment_system/airplane_entertainment_system/airplane_entertainment_system.dart';
import 'package:airplane_entertainment_system/l10n/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:just_audio/just_audio.dart';
import 'package:music_repository/music_repository.dart';

class App extends StatelessWidget {
const App({super.key});

@override
Widget build(BuildContext context) {
return AesLayout(
child: MaterialApp(
theme: const AesTheme().themeData,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const AirplaneEntertainmentSystemScreen(),
return MultiRepositoryProvider(
providers: [
RepositoryProvider(
create: (context) => MusicRepository(),
),
RepositoryProvider(
create: (context) => AudioPlayer(),
),
],
child: AesLayout(
child: MaterialApp(
theme: const AesTheme().themeData,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const AirplaneEntertainmentSystemScreen(),
),
),
);
}
Expand Down
1 change: 1 addition & 0 deletions lib/music_player/cubit/cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'music_player_cubit.dart';
115 changes: 115 additions & 0 deletions lib/music_player/cubit/music_player_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:just_audio/just_audio.dart';
import 'package:music_repository/music_repository.dart';

part 'music_player_state.dart';

class MusicPlayerCubit extends Cubit<MusicPlayerState> {
MusicPlayerCubit({
required MusicRepository musicRepository,
required AudioPlayer player,
}) : _musicRepository = musicRepository,
_player = player,
super(const MusicPlayerState()) {
_isPlayingSubscription = _player.playingStream.listen(_onIsPlayingChanged);
_progressSubscription = _player.positionStream.listen(_onProgressChanged);
_trackSubscription =
_player.currentIndexStream.listen(_onTrackIndexChanged);
}

final MusicRepository _musicRepository;
late final StreamSubscription<bool> _isPlayingSubscription;
late final StreamSubscription<Duration> _progressSubscription;
late final StreamSubscription<int?> _trackSubscription;
final AudioPlayer _player;

void _onIsPlayingChanged(bool isPlaying) {
emit(state.copyWith(isPlaying: isPlaying));
}

void _onProgressChanged(Duration position) {
final duration = _player.duration;
if (duration == null) return;
final progress = position.inMilliseconds / duration.inMilliseconds;
emit(state.copyWith(progress: progress));
}

void _onTrackIndexChanged(int? index) {
if (index == null) {
emit(MusicPlayerState(tracks: state.tracks));
} else {
emit(state.copyWith(currentTrackIndex: index));
}
}

void initialize() {
final tracks = _musicRepository.getTracks();

if (_player.audioSource == null) {
final playlist = ConcatenatingAudioSource(
children: tracks.map((track) => AudioSource.asset(track.path)).toList(),
);
_player.setAudioSource(playlist);
}

emit(state.copyWith(tracks: tracks));
}

void playTrack(MusicTrack track) {
if (track == state.currentTrack) {
return togglePlayPause();
}
_player
..seek(Duration.zero, index: track.index)
..play();
}

void togglePlayPause() {
if (state.currentTrack == null) return;
if (state.isPlaying) {
_player.pause();
} else {
_player.play();
}
}

void seek(double progress) {
final duration = _player.duration;
if (duration != null) {
_player.seek(duration * progress);
}
}

void next() {
if (state.currentTrack == null) return;
_player.seekToNext();
}

void previous() {
if (state.currentTrack == null) return;
_player.seekToPrevious();
}

void toggleLoop() {
final loop = !state.isLoop;
_player.setLoopMode(loop ? LoopMode.one : LoopMode.off);
emit(state.copyWith(isLoop: !state.isLoop));
}

void toggleShuffle() {
final shuffle = !state.isShuffle;
_player.setShuffleModeEnabled(shuffle);
emit(state.copyWith(isShuffle: !state.isShuffle));
}

@override
Future<void> close() {
_isPlayingSubscription.cancel();
_progressSubscription.cancel();
_trackSubscription.cancel();
return super.close();
}
}
51 changes: 51 additions & 0 deletions lib/music_player/cubit/music_player_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
part of 'music_player_cubit.dart';

class MusicPlayerState extends Equatable {
const MusicPlayerState({
this.tracks = const [],
this.currentTrackIndex,
this.progress = 0.0,
this.isPlaying = false,
this.isLoop = false,
this.isShuffle = false,
});

final List<MusicTrack> tracks;
final int? currentTrackIndex;
final double progress;
final bool isPlaying;
final bool isLoop;
final bool isShuffle;

MusicTrack? get currentTrack => tracks.isNotEmpty && currentTrackIndex != null
? tracks[currentTrackIndex!]
: null;

@override
List<Object?> get props => [
tracks,
currentTrackIndex,
isPlaying,
progress,
isLoop,
isShuffle,
];

MusicPlayerState copyWith({
List<MusicTrack>? tracks,
int? currentTrackIndex,
double? progress,
bool? isPlaying,
bool? isLoop,
bool? isShuffle,
}) {
return MusicPlayerState(
tracks: tracks ?? this.tracks,
currentTrackIndex: currentTrackIndex ?? this.currentTrackIndex,
isPlaying: isPlaying ?? this.isPlaying,
progress: progress ?? this.progress,
isLoop: isLoop ?? this.isLoop,
isShuffle: isShuffle ?? this.isShuffle,
);
}
}
1 change: 1 addition & 0 deletions lib/music_player/music_player.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export 'cubit/cubit.dart';
export 'view/view.dart';
export 'widgets/widgets.dart';
Loading

0 comments on commit f3dc8f5

Please sign in to comment.