diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/ios/Podfile @@ -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 diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..30c7b49 --- /dev/null +++ b/ios/Podfile.lock @@ -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 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 3b09427..76a8fb3 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B6EE9B8F7C4A1AB8842C431E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A79AD057BF9F6ED42D00114A /* Pods_RunnerTests.framework */; }; + B80E9ADB53F6271220626B22 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8DC0BEF752AFEC9F1280B8E /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -42,12 +44,22 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 16386CFB1C4FD7E3B3548D7F /* Pods-Runner.profile-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-development.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-development.xcconfig"; sourceTree = ""; }; + 2A7B56159D8A78720ABEFF9D /* Pods-Runner.profile-production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-production.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-production.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 38276CF3373ADB8C57192CEB /* Pods-RunnerTests.debug-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug-development.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug-development.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3EBCCB0FCD106BB2D6D269F7 /* Pods-RunnerTests.release-production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release-production.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release-production.xcconfig"; sourceTree = ""; }; + 74423F7865C70A54CED51951 /* Pods-Runner.release-staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-staging.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-staging.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 79620DBC7D29149AF12CE27E /* Pods-RunnerTests.profile-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile-development.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile-development.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7E905AF63340BE82AC69E208 /* Pods-Runner.debug-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-development.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-development.xcconfig"; sourceTree = ""; }; + 889D111CA9ED9FCC161AD240 /* Pods-RunnerTests.release-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release-development.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release-development.xcconfig"; sourceTree = ""; }; + 8C21BEEFA5DEAB7852BA525F /* Pods-RunnerTests.debug-staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug-staging.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug-staging.xcconfig"; sourceTree = ""; }; + 954CF3856663EDD4BA165445 /* Pods-RunnerTests.profile-staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile-staging.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile-staging.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -55,6 +67,16 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A3860D8395F4A00FFC8B7C4F /* Pods-Runner.release-production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-production.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-production.xcconfig"; sourceTree = ""; }; + A79AD057BF9F6ED42D00114A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C09791519176263CAD2B4538 /* Pods-Runner.release-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-development.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-development.xcconfig"; sourceTree = ""; }; + D2B137C96D14F81FB49C8085 /* Pods-RunnerTests.release-staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release-staging.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release-staging.xcconfig"; sourceTree = ""; }; + D42319EFFCF973ED666DC5BF /* Pods-RunnerTests.debug-production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug-production.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug-production.xcconfig"; sourceTree = ""; }; + D8CAC321CB996BC1DDAB2BDC /* Pods-Runner.debug-production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-production.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-production.xcconfig"; sourceTree = ""; }; + DC0C45A764B9EC4EFA6880B7 /* Pods-Runner.profile-staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-staging.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-staging.xcconfig"; sourceTree = ""; }; + E42CBDF74C76AA901DD7E061 /* Pods-RunnerTests.profile-production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile-production.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile-production.xcconfig"; sourceTree = ""; }; + EEF14231BAC0AA012407B77C /* Pods-Runner.debug-staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-staging.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-staging.xcconfig"; sourceTree = ""; }; + F8DC0BEF752AFEC9F1280B8E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -62,13 +84,31 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B80E9ADB53F6271220626B22 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD36591B1A5AA110A0C8DC5F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B6EE9B8F7C4A1AB8842C431E /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { + 1226921AB210749176DFBE34 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F8DC0BEF752AFEC9F1280B8E /* Pods_Runner.framework */, + A79AD057BF9F6ED42D00114A /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C807B294A618700263BE5 /* RunnerTests.swift */, @@ -94,6 +134,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 9AC49F8DDAD229EECE72C580 /* Pods */, + 1226921AB210749176DFBE34 /* Frameworks */, ); sourceTree = ""; }; @@ -121,15 +163,43 @@ path = Runner; sourceTree = ""; }; + 9AC49F8DDAD229EECE72C580 /* Pods */ = { + isa = PBXGroup; + children = ( + D8CAC321CB996BC1DDAB2BDC /* Pods-Runner.debug-production.xcconfig */, + EEF14231BAC0AA012407B77C /* Pods-Runner.debug-staging.xcconfig */, + 7E905AF63340BE82AC69E208 /* Pods-Runner.debug-development.xcconfig */, + A3860D8395F4A00FFC8B7C4F /* Pods-Runner.release-production.xcconfig */, + 74423F7865C70A54CED51951 /* Pods-Runner.release-staging.xcconfig */, + C09791519176263CAD2B4538 /* Pods-Runner.release-development.xcconfig */, + 2A7B56159D8A78720ABEFF9D /* Pods-Runner.profile-production.xcconfig */, + DC0C45A764B9EC4EFA6880B7 /* Pods-Runner.profile-staging.xcconfig */, + 16386CFB1C4FD7E3B3548D7F /* Pods-Runner.profile-development.xcconfig */, + D42319EFFCF973ED666DC5BF /* Pods-RunnerTests.debug-production.xcconfig */, + 8C21BEEFA5DEAB7852BA525F /* Pods-RunnerTests.debug-staging.xcconfig */, + 38276CF3373ADB8C57192CEB /* Pods-RunnerTests.debug-development.xcconfig */, + 3EBCCB0FCD106BB2D6D269F7 /* Pods-RunnerTests.release-production.xcconfig */, + D2B137C96D14F81FB49C8085 /* Pods-RunnerTests.release-staging.xcconfig */, + 889D111CA9ED9FCC161AD240 /* Pods-RunnerTests.release-development.xcconfig */, + E42CBDF74C76AA901DD7E061 /* Pods-RunnerTests.profile-production.xcconfig */, + 954CF3856663EDD4BA165445 /* Pods-RunnerTests.profile-staging.xcconfig */, + 79620DBC7D29149AF12CE27E /* Pods-RunnerTests.profile-development.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { + 331C8080294A63A400263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + E3554CA17BDAC88723FDEC23 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + AD36591B1A5AA110A0C8DC5F /* Frameworks */, ); buildRules = ( ); @@ -145,12 +215,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 85B2C43025423ABA02650C00 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 5E263026FBDF3BE0C5D52E57 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -167,11 +239,11 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - BuildIndependentTargetsInParallel = NO; + BuildIndependentTargetsInParallel = NO; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { - 331C8080294A63A400263BE5 = { + 331C8080294A63A400263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 97C146ED1CF9000F007C117D; }; @@ -201,7 +273,7 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { + 331C807F294A63A400263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -238,6 +310,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 5E263026FBDF3BE0C5D52E57 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 85B2C43025423ABA02650C00 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,10 +364,32 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + E3554CA17BDAC88723FDEC23 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { + 331C807D294A63A400263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -373,7 +506,7 @@ "@executable_path/Frameworks", ); PACKAGE_CONFIG = .dart_tool/package_config.json; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -384,13 +517,14 @@ }; 3C9C551B2B07AC99000E5FCD /* Debug-production */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D42319EFFCF973ED666DC5BF /* Pods-RunnerTests.debug-production.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.RunnerTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -466,7 +600,7 @@ "@executable_path/Frameworks", ); PACKAGE_CONFIG = .dart_tool/package_config.json; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -476,13 +610,14 @@ }; 3C9C551E2B07ACA4000E5FCD /* Release-production */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 3EBCCB0FCD106BB2D6D269F7 /* Pods-RunnerTests.release-production.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.RunnerTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -554,7 +689,7 @@ "@executable_path/Frameworks", ); PACKAGE_CONFIG = .dart_tool/package_config.json; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -564,13 +699,14 @@ }; 3C9C55242B07ACBD000E5FCD /* Profile-production */ = { isa = XCBuildConfiguration; + baseConfigurationReference = E42CBDF74C76AA901DD7E061 /* Pods-RunnerTests.profile-production.xcconfig */; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; + BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -647,7 +783,7 @@ "@executable_path/Frameworks", ); PACKAGE_CONFIG = .dart_tool/package_config.json; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.dev; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.dev"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -658,13 +794,14 @@ }; 3C9C55272B07ACE7000E5FCD /* Debug-development */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 38276CF3373ADB8C57192CEB /* Pods-RunnerTests.debug-development.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.RunnerTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -738,7 +875,7 @@ "@executable_path/Frameworks", ); PACKAGE_CONFIG = .dart_tool/package_config.json; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.dev; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.dev"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -748,13 +885,14 @@ }; 3C9C552D2B07AD09000E5FCD /* Profile-development */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 79620DBC7D29149AF12CE27E /* Pods-RunnerTests.profile-development.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.RunnerTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -806,9 +944,9 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = "Release-development"; @@ -828,7 +966,7 @@ "@executable_path/Frameworks", ); PACKAGE_CONFIG = .dart_tool/package_config.json; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.dev; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.dev"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -838,13 +976,14 @@ }; 3C9C55302B07AD1B000E5FCD /* Release-development */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 889D111CA9ED9FCC161AD240 /* Pods-RunnerTests.release-development.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.RunnerTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -921,7 +1060,7 @@ "@executable_path/Frameworks", ); PACKAGE_CONFIG = .dart_tool/package_config.json; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.stg; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.stg"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -932,13 +1071,14 @@ }; 3C9C55332B07AD26000E5FCD /* Debug-staging */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 8C21BEEFA5DEAB7852BA525F /* Pods-RunnerTests.debug-staging.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.RunnerTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -1014,7 +1154,7 @@ "@executable_path/Frameworks", ); PACKAGE_CONFIG = .dart_tool/package_config.json; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.stg; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.stg"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -1024,13 +1164,14 @@ }; 3C9C55362B07AD41000E5FCD /* Release-staging */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D2B137C96D14F81FB49C8085 /* Pods-RunnerTests.release-staging.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.RunnerTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -1102,7 +1243,7 @@ "@executable_path/Frameworks", ); PACKAGE_CONFIG = .dart_tool/package_config.json; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.stg; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.stg"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -1112,13 +1253,14 @@ }; 3C9C55392B07AD4B000E5FCD /* Profile-staging */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 954CF3856663EDD4BA165445 /* Pods-RunnerTests.profile-staging.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.vgv.experience.airplane-entertainment-system.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.vgv.experience.airplane-entertainment-system.RunnerTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -1128,7 +1270,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 3C9C551B2B07AC99000E5FCD /* Debug-production */, diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 46a60d5..c582661 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -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(), + ), ), ); } diff --git a/lib/music_player/cubit/cubit.dart b/lib/music_player/cubit/cubit.dart new file mode 100644 index 0000000..21b17b5 --- /dev/null +++ b/lib/music_player/cubit/cubit.dart @@ -0,0 +1 @@ +export 'music_player_cubit.dart'; diff --git a/lib/music_player/cubit/music_player_cubit.dart b/lib/music_player/cubit/music_player_cubit.dart new file mode 100644 index 0000000..3f9ab3a --- /dev/null +++ b/lib/music_player/cubit/music_player_cubit.dart @@ -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 { + 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 _isPlayingSubscription; + late final StreamSubscription _progressSubscription; + late final StreamSubscription _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 close() { + _isPlayingSubscription.cancel(); + _progressSubscription.cancel(); + _trackSubscription.cancel(); + return super.close(); + } +} diff --git a/lib/music_player/cubit/music_player_state.dart b/lib/music_player/cubit/music_player_state.dart new file mode 100644 index 0000000..468a2eb --- /dev/null +++ b/lib/music_player/cubit/music_player_state.dart @@ -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 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 get props => [ + tracks, + currentTrackIndex, + isPlaying, + progress, + isLoop, + isShuffle, + ]; + + MusicPlayerState copyWith({ + List? 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, + ); + } +} diff --git a/lib/music_player/music_player.dart b/lib/music_player/music_player.dart index 3b46d13..003b3c1 100644 --- a/lib/music_player/music_player.dart +++ b/lib/music_player/music_player.dart @@ -1,2 +1,3 @@ +export 'cubit/cubit.dart'; export 'view/view.dart'; export 'widgets/widgets.dart'; diff --git a/lib/music_player/view/music_player_page.dart b/lib/music_player/view/music_player_page.dart index 0bc6f0e..2aaa16b 100644 --- a/lib/music_player/view/music_player_page.dart +++ b/lib/music_player/view/music_player_page.dart @@ -2,6 +2,8 @@ import 'package:aes_ui/aes_ui.dart'; import 'package:airplane_entertainment_system/l10n/l10n.dart'; import 'package:airplane_entertainment_system/music_player/music_player.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:music_repository/music_repository.dart'; class MusicPlayerPage extends StatelessWidget { const MusicPlayerPage({super.key}); @@ -10,12 +12,22 @@ class MusicPlayerPage extends StatelessWidget { Widget build(BuildContext context) { final layout = AesLayout.of(context); - return switch (layout) { - AesLayoutData.small => const _SmallMusicPlayerPage(), - AesLayoutData.medium || - AesLayoutData.large => - const _LargeMusicPlayerPage(), - }; + return BlocProvider( + create: (_) => MusicPlayerCubit( + musicRepository: context.read(), + player: context.read(), + )..initialize(), + child: Builder( + builder: (context) { + return switch (layout) { + AesLayoutData.small => const _SmallMusicPlayerPage(), + AesLayoutData.medium || + AesLayoutData.large => + const _LargeMusicPlayerPage(), + }; + }, + ), + ); } } @@ -85,7 +97,7 @@ class MusicFloatingButton extends StatelessWidget { children: [ Transform.scale( scale: 0.2, - child: const MusicVisualizer(), + child: const _PlayingMusicVisualizer(), ), SizedBox( height: 50, @@ -111,7 +123,10 @@ class MusicFloatingButton extends StatelessWidget { constraints: const BoxConstraints( maxHeight: 380, ), - builder: (_) => const _MusicBottomSheet(), + builder: (_) => BlocProvider.value( + value: context.read(), + child: const _MusicBottomSheet(), + ), ); }, child: Center( @@ -147,19 +162,25 @@ class _MusicBottomSheet extends StatelessWidget { child: MusicPlayerView(), ), ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _musicItems[0]['title']!, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - const Text(' - '), - Text(_musicItems[0]['artist']!), - ], + BlocSelector( + selector: (state) => state.currentTrack, + builder: (context, track) { + if (track == null) return const SizedBox(); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + track.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const Text(' - '), + Text(track.artist), + ], + ); + }, ), ], ), @@ -200,13 +221,17 @@ class MusicPlayerView extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Icon(Icons.first_page_rounded), + IconButton( + onPressed: () => + context.read().previous(), + icon: const Icon(Icons.first_page_rounded), + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Stack( alignment: Alignment.center, children: [ - const MusicVisualizer(), + const _PlayingMusicVisualizer(), SizedBox( height: 300, width: 300, @@ -233,11 +258,24 @@ class MusicPlayerView extends StatelessWidget { shape: BoxShape.circle, color: Colors.black.withOpacity(0.5), ), - child: const Center( - child: Icon( - Icons.pause, - size: 40, - color: Colors.white, + child: Center( + child: InkWell( + onTap: () => context + .read() + .togglePlayPause(), + child: BlocSelector( + selector: (state) => state.isPlaying, + builder: (context, isPlaying) { + return Icon( + isPlaying + ? Icons.pause + : Icons.play_arrow, + size: 40, + color: Colors.white, + ); + }, + ), ), ), ), @@ -245,14 +283,28 @@ class MusicPlayerView extends StatelessWidget { ], ), ), - const Icon(Icons.last_page_rounded), + IconButton( + onPressed: () => context.read().next(), + icon: const Icon(Icons.last_page_rounded), + ), ], ), const SizedBox(height: 40), Flexible( child: Row( children: [ - const Icon(Icons.shuffle), + BlocSelector( + selector: (state) => state.isShuffle, + builder: (context, enabled) { + return IconButton( + color: enabled ? Colors.red : null, + onPressed: () => context + .read() + .toggleShuffle(), + icon: const Icon(Icons.shuffle), + ); + }, + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Card( @@ -260,17 +312,35 @@ class MusicPlayerView extends StatelessWidget { color: Colors.transparent, child: SizedBox( width: 360, - child: Slider( - inactiveColor: Colors.grey, - activeColor: Colors.red, - thumbColor: Colors.white, - value: 0, - onChanged: (_) {}, + child: BlocSelector( + selector: (state) => state.progress, + builder: (context, progress) { + return Slider( + inactiveColor: Colors.grey, + activeColor: Colors.red, + thumbColor: Colors.white, + value: progress, + onChanged: (value) => context + .read() + .seek(value), + ); + }, ), ), ), ), - const Icon(Icons.repeat_rounded), + BlocSelector( + selector: (state) => state.isLoop, + builder: (context, enabled) { + return IconButton( + color: enabled ? Colors.red : null, + onPressed: () => + context.read().toggleLoop(), + icon: const Icon(Icons.repeat_rounded), + ); + }, + ), ], ), ), @@ -283,6 +353,18 @@ class MusicPlayerView extends StatelessWidget { } } +class _PlayingMusicVisualizer extends StatelessWidget { + const _PlayingMusicVisualizer(); + + @override + Widget build(BuildContext context) { + final isPlaying = context.select( + (cubit) => cubit.state.isPlaying, + ); + return MusicVisualizer(isActive: isPlaying); + } +} + class MusicMenuView extends StatelessWidget { const MusicMenuView({super.key, this.padding}); @@ -312,20 +394,35 @@ class MusicMenuView extends StatelessWidget { child: const _MusicMenuHeader(), ), Flexible( - child: ListView.separated( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - padding: padding?.copyWith(top: 0), - itemBuilder: (context, pos) => _MusicMenuItem( - title: _musicItems[pos]['title']!, - artist: _musicItems[pos]['artist']!, - trackPosition: pos + 1, - isPlaying: pos == 0, - ), - separatorBuilder: (_, __) => const Divider( - color: Colors.transparent, + child: BlocSelector, MusicTrack?, bool)>( + selector: (state) => ( + state.tracks, + state.currentTrack, + state.isPlaying, ), - itemCount: _musicItems.length, + builder: (context, state) { + final (tracks, current, isPlaying) = state; + return ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: padding?.copyWith(top: 0), + itemBuilder: (context, pos) { + final track = tracks[pos]; + return _MusicMenuItem( + track: track, + isCurrent: track == current, + isPlaying: isPlaying, + onTap: () => + context.read().playTrack(track), + ); + }, + separatorBuilder: (_, __) => const Divider( + color: Colors.transparent, + ), + itemCount: tracks.length, + ); + }, ), ), ], @@ -335,20 +432,15 @@ class MusicMenuView extends StatelessWidget { } } -const _musicItems = [ - {'title': 'Starlight', 'artist': 'Muse'}, - {'title': 'E a verdade', 'artist': 'Riles'}, - {'title': 'Dance Monkey', 'artist': 'Tones and I'}, - {'title': 'La Campanella', 'artist': 'Chopin'}, - {'title': 'Calm Down', 'artist': 'Rema'}, -]; - class _MusicMenuHeader extends StatelessWidget { const _MusicMenuHeader(); @override Widget build(BuildContext context) { final l10n = context.l10n; + final count = context.select( + (cubit) => cubit.state.tracks.length, + ); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -361,7 +453,7 @@ class _MusicMenuHeader extends StatelessWidget { ), const SizedBox(height: 10), Text( - '${l10n.multipleArtists}\n26 ${l10n.songs}', + '${l10n.multipleArtists}\n$count ${l10n.songs}', style: TextStyle( color: Colors.grey.shade800, ), @@ -373,66 +465,71 @@ class _MusicMenuHeader extends StatelessWidget { class _MusicMenuItem extends StatelessWidget { const _MusicMenuItem({ - required this.title, - required this.artist, - required this.trackPosition, + required this.track, + this.onTap, + this.isCurrent = false, this.isPlaying = false, }); - final String title; - final String artist; - final int trackPosition; + final MusicTrack track; + final bool isCurrent; final bool isPlaying; + final VoidCallback? onTap; @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; - return SizedBox( - height: 90, - child: DecoratedBox( - decoration: const BoxDecoration(color: Colors.white), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.all(20), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 20), - child: Text(trackPosition.toString()), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle(fontWeight: FontWeight.bold), - overflow: TextOverflow.ellipsis, - ), - Text( - artist, - style: const TextStyle(color: Colors.grey), - overflow: TextOverflow.ellipsis, - ), - ], + return InkWell( + onTap: onTap, + child: SizedBox( + height: 90, + child: DecoratedBox( + decoration: const BoxDecoration(color: Colors.white), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 20), + child: Text((track.index + 1).toString()), ), - ), - Icon( - isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, - ), - ], - ), - ), - if (isPlaying) - SizedBox( - height: size.height, - width: 5, - child: const DecoratedBox( - decoration: BoxDecoration(color: Colors.red), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + track.title, + style: const TextStyle(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + Text( + track.artist, + style: const TextStyle(color: Colors.grey), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Icon( + isCurrent && isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + ), + ], ), ), - ], + if (isCurrent) + SizedBox( + height: size.height, + width: 5, + child: const DecoratedBox( + decoration: BoxDecoration(color: Colors.red), + ), + ), + ], + ), ), ), ); diff --git a/lib/music_player/widgets/music_visualizer.dart b/lib/music_player/widgets/music_visualizer.dart index a24c865..dc49023 100644 --- a/lib/music_player/widgets/music_visualizer.dart +++ b/lib/music_player/widgets/music_visualizer.dart @@ -4,16 +4,20 @@ import 'dart:math'; import 'package:flutter/material.dart'; class MusicVisualizer extends StatefulWidget { - const MusicVisualizer({super.key}); + const MusicVisualizer({required this.isActive, super.key}); + + final bool isActive; @override - State createState() => _MusicVisualizerState(); + State createState() => MusicVisualizerState(); } -class _MusicVisualizerState extends State - with SingleTickerProviderStateMixin { +@visibleForTesting +class MusicVisualizerState extends State + with TickerProviderStateMixin { bool ready = false; late final AnimationController animationController; + late final AnimationController extensionController; late final List> spectrogram; late int spectrogramIndex; late List> frequencyTweens; @@ -25,6 +29,13 @@ class _MusicVisualizerState extends State vsync: this, duration: const Duration(milliseconds: 50), ); + extensionController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + if (widget.isActive) { + extensionController.value = 1; + } } @override @@ -41,6 +52,16 @@ class _MusicVisualizerState extends State _loadSpectrogram(); } + @override + void didUpdateWidget(covariant MusicVisualizer oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isActive) { + extensionController.forward(); + } else { + extensionController.reverse(); + } + } + Future _loadSpectrogram() async { final spectrogramData = await DefaultAssetBundle.of(context).loadString( 'assets/spectrogram.json', @@ -90,11 +111,15 @@ class _MusicVisualizerState extends State } return AnimatedBuilder( - animation: animationController, + animation: Listenable.merge([animationController, extensionController]), builder: (context, _) => CustomPaint( painter: _MusicVisualizerPainter( channels: frequencyTweens - .map((tween) => tween.transform(animationController.value)) + .map( + (tween) => + tween.transform(animationController.value) * + extensionController.value, + ) .toList(), ), ), diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..df18b27 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,12 @@ import FlutterMacOS import Foundation +import audio_session +import just_audio +import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..c795730 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# 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', 'ephemeral', '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 Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_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_macos_build_settings(target) + end +end diff --git a/pubspec.lock b/pubspec.lock index db31e8e..f41396c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -40,6 +40,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + audio_session: + dependency: transitive + description: + name: audio_session + sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261" + url: "https://pub.dev" + source: hosted + version: "0.1.21" bloc: dependency: "direct main" description: @@ -153,7 +161,7 @@ packages: source: hosted version: "0.4.1" equatable: - dependency: transitive + dependency: "direct main" description: name: equatable sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 @@ -168,6 +176,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" file: dependency: transitive description: @@ -176,6 +192,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -215,6 +239,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -295,6 +324,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" + just_audio: + dependency: "direct main" + description: + name: just_audio + sha256: ee50602364ba83fa6308f5512dd560c713ec3e1f2bc75f0db43618f0d82ef71a + url: "https://pub.dev" + source: hosted + version: "0.9.39" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + sha256: "0243828cce503c8366cc2090cefb2b3c871aa8ed2f520670d76fd47aa1ab2790" + url: "https://pub.dev" + source: hosted + version: "4.3.0" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + sha256: "0edb481ad4aa1ff38f8c40f1a3576013c3420bf6669b686fe661627d49bc606c" + url: "https://pub.dev" + source: hosted + version: "0.4.11" leak_tracker: dependency: transitive description: @@ -367,6 +420,13 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + music_repository: + dependency: "direct main" + description: + path: "packages/music_repository" + relative: true + source: path + version: "0.1.0+1" nested: dependency: transitive description: @@ -407,6 +467,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e" + url: "https://pub.dev" + source: hosted + version: "2.2.7" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -415,6 +523,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -439,6 +563,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shelf: dependency: transitive description: @@ -500,6 +632,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -572,6 +712,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + url: "https://pub.dev" + source: hosted + version: "4.4.2" vector_graphics_codec: dependency: transitive description: @@ -644,6 +792,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 25115fc..a0d7b5d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,12 +10,16 @@ dependencies: aes_ui: path: packages/aes_ui bloc: ^8.1.4 + equatable: ^2.0.5 flutter: sdk: flutter flutter_bloc: ^8.1.6 flutter_localizations: sdk: flutter intl: ^0.19.0 + just_audio: ^0.9.39 + music_repository: + path: packages/music_repository dev_dependencies: bloc_test: ^9.1.7 diff --git a/test/airplane_entertainment_system/view/airplane_entertainment_system_screen_test.dart b/test/airplane_entertainment_system/view/airplane_entertainment_system_screen_test.dart index 21d400d..495018f 100644 --- a/test/airplane_entertainment_system/view/airplane_entertainment_system_screen_test.dart +++ b/test/airplane_entertainment_system/view/airplane_entertainment_system_screen_test.dart @@ -4,11 +4,31 @@ import 'package:airplane_entertainment_system/music_player/music_player.dart'; import 'package:airplane_entertainment_system/overview/overview.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:music_repository/music_repository.dart'; import '../../helpers/helpers.dart'; void main() { group('$AirplaneEntertainmentSystemScreen', () { + late MusicRepository musicRepository; + late AudioPlayer audioPlayer; + + setUp(() { + musicRepository = MockMusicRepository(); + when(musicRepository.getTracks).thenReturn(const []); + + audioPlayer = MockAudioPlayer(); + when(() => audioPlayer.audioSource).thenReturn(HlsAudioSource(Uri())); + when(() => audioPlayer.positionStream) + .thenAnswer((_) => const Stream.empty()); + when(() => audioPlayer.playingStream) + .thenAnswer((_) => const Stream.empty()); + when(() => audioPlayer.currentIndexStream) + .thenAnswer((_) => const Stream.empty()); + }); + testWidgets('shows $AesNavigationRail on large screens', (tester) async { await tester.binding.setSurfaceSize(const Size(1600, 1200)); await tester.pumpApp( @@ -60,6 +80,8 @@ void main() { await tester.pumpApp( const AirplaneEntertainmentSystemScreen(), layout: AesLayoutData.small, + musicRepository: musicRepository, + audioPlayer: audioPlayer, ); await tester.tap(find.byIcon(Icons.music_note)); @@ -76,6 +98,8 @@ void main() { await tester.pumpApp( const AirplaneEntertainmentSystemScreen(), layout: layout, + musicRepository: musicRepository, + audioPlayer: audioPlayer, ); await tester.tap(find.byIcon(Icons.music_note)); diff --git a/test/helpers/pump_experience.dart b/test/helpers/pump_experience.dart index 3700d36..b72b818 100644 --- a/test/helpers/pump_experience.dart +++ b/test/helpers/pump_experience.dart @@ -1,10 +1,23 @@ import 'package:aes_ui/aes_ui.dart'; import 'package:airplane_entertainment_system/l10n/l10n.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:music_repository/music_repository.dart'; + +class MockMusicRepository extends Mock implements MusicRepository {} + +class MockAudioPlayer extends Mock implements AudioPlayer {} extension PumpApp on WidgetTester { - Future pumpApp(Widget widget, {AesLayoutData? layout}) async { + Future pumpApp( + Widget widget, { + AesLayoutData? layout, + MusicRepository? musicRepository, + AudioPlayer? audioPlayer, + }) async { if (layout == AesLayoutData.large) { await binding.setSurfaceSize(const Size(1600, 1200)); addTearDown(() => binding.setSurfaceSize(null)); @@ -13,10 +26,20 @@ extension PumpApp on WidgetTester { return pumpWidget( AesLayout( data: layout, - child: MaterialApp( - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - home: widget, + child: MultiRepositoryProvider( + providers: [ + RepositoryProvider( + create: (context) => musicRepository ?? MockMusicRepository(), + ), + RepositoryProvider( + create: (context) => audioPlayer ?? MockAudioPlayer(), + ), + ], + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: widget, + ), ), ), ); diff --git a/test/music_player/cubit/music_player_cubit_test.dart b/test/music_player/cubit/music_player_cubit_test.dart new file mode 100644 index 0000000..918021c --- /dev/null +++ b/test/music_player/cubit/music_player_cubit_test.dart @@ -0,0 +1,362 @@ +import 'package:airplane_entertainment_system/music_player/cubit/music_player_cubit.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:music_repository/music_repository.dart'; + +import '../../helpers/pump_experience.dart'; + +class _FakeAudioSource extends Fake implements AudioSource {} + +void main() { + group('MusicPlayerCubit', () { + const tracks = [ + MusicTrack(index: 0, title: 'Title', artist: 'Artist', path: 'path'), + ]; + late MusicRepository musicRepository; + late AudioPlayer audioPlayer; + + setUpAll(() { + registerFallbackValue(_FakeAudioSource()); + registerFallbackValue(LoopMode.off); + }); + + setUp(() { + musicRepository = MockMusicRepository(); + + audioPlayer = MockAudioPlayer(); + when(() => audioPlayer.positionStream) + .thenAnswer((_) => const Stream.empty()); + when(() => audioPlayer.playingStream) + .thenAnswer((_) => const Stream.empty()); + when(() => audioPlayer.currentIndexStream) + .thenAnswer((_) => const Stream.empty()); + when(() => audioPlayer.setAudioSource(any())) + .thenAnswer((_) async => null); + when(() => audioPlayer.seek(any(), index: any(named: 'index'))) + .thenAnswer((_) async {}); + when(audioPlayer.pause).thenAnswer((_) async {}); + when(audioPlayer.play).thenAnswer((_) async {}); + when(audioPlayer.seekToNext).thenAnswer((_) async {}); + when(audioPlayer.seekToPrevious).thenAnswer((_) async {}); + when(() => audioPlayer.setLoopMode(any())).thenAnswer((_) async {}); + when(() => audioPlayer.setShuffleModeEnabled(any())) + .thenAnswer((_) async {}); + }); + + MusicPlayerCubit build() => MusicPlayerCubit( + musicRepository: musicRepository, + player: audioPlayer, + ); + + blocTest( + 'initial state is default', + build: build, + verify: (cubit) { + expect(cubit.state, const MusicPlayerState()); + }, + ); + + blocTest( + 'initial state is default', + build: build, + verify: (cubit) { + expect(cubit.state, const MusicPlayerState()); + }, + ); + + blocTest( + 'updates [isPlaying] when [playingStream] emits', + setUp: () => when(() => audioPlayer.playingStream) + .thenAnswer((_) => Stream.value(true)), + build: build, + expect: () => const [ + MusicPlayerState(isPlaying: true), + ], + ); + + blocTest( + 'updates [progress] when [positionStream] emits', + setUp: () { + when(() => audioPlayer.duration) + .thenReturn(const Duration(seconds: 10)); + when(() => audioPlayer.positionStream) + .thenAnswer((_) => Stream.value(const Duration(seconds: 1))); + }, + build: build, + expect: () => const [ + MusicPlayerState(progress: 0.1), + ], + ); + + blocTest( + 'updates [currentTrackIndex] when [currentIndexStream] emits value', + setUp: () => when(() => audioPlayer.currentIndexStream) + .thenAnswer((_) => Stream.value(0)), + build: build, + expect: () => const [ + MusicPlayerState(currentTrackIndex: 0), + ], + ); + + blocTest( + 'clears state when [currentIndexStream] emits null (keeping tracks)', + setUp: () => when(() => audioPlayer.currentIndexStream) + .thenAnswer((_) => Stream.value(null)), + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + isPlaying: true, + ), + build: build, + expect: () => const [ + MusicPlayerState(tracks: tracks), + ], + ); + + group('initialize', () { + setUp(() { + when(musicRepository.getTracks).thenReturn(tracks); + when(() => audioPlayer.audioSource).thenReturn(_FakeAudioSource()); + when(() => audioPlayer.audioSource).thenReturn(null); + }); + + blocTest( + 'loads music tracks', + build: build, + act: (cubit) => cubit.initialize(), + expect: () => const [ + MusicPlayerState(tracks: tracks), + ], + ); + + blocTest( + 'sets tracks as audio source in player if it is not set', + setUp: () => when(() => audioPlayer.audioSource).thenReturn(null), + build: build, + act: (cubit) => cubit.initialize(), + verify: (_) => verify( + () => audioPlayer.setAudioSource( + any(that: isA()), + ), + ).called(1), + ); + }); + + group('playTrack', () { + blocTest( + 'calls [audioPlayer.seek] with at the track index', + build: build, + act: (cubit) => cubit.playTrack(tracks[0]), + verify: (_) => verify( + () => audioPlayer.seek( + Duration.zero, + index: 0, + ), + ).called(1), + ); + + blocTest( + 'calls [audioPlayer.play]', + build: build, + act: (cubit) => cubit.playTrack(tracks[0]), + verify: (_) => verify(audioPlayer.play).called(1), + ); + + blocTest( + 'calls [audioPlayer.pause] when the track is already playing', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + isPlaying: true, + ), + act: (cubit) => cubit.playTrack(tracks[0]), + verify: (_) => verify(audioPlayer.pause).called(1), + ); + }); + + group('togglePlayPause', () { + blocTest( + 'calls [audioPlayer.play] when the player is paused', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + ), + act: (cubit) => cubit.togglePlayPause(), + verify: (_) => verify(audioPlayer.play).called(1), + ); + + blocTest( + 'calls [audioPlayer.pause] when the player is playing', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + isPlaying: true, + ), + act: (cubit) => cubit.togglePlayPause(), + verify: (_) => verify(audioPlayer.pause).called(1), + ); + }); + + group('seek', () { + setUp(() { + when(() => audioPlayer.duration) + .thenReturn(const Duration(seconds: 10)); + }); + + blocTest( + 'calls [audioPlayer.seek] with the corresponding duration and', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + ), + act: (cubit) => cubit.seek(0.5), + verify: (_) => verify( + () => audioPlayer.seek( + const Duration(seconds: 5), + ), + ).called(1), + ); + }); + + group('next', () { + blocTest( + 'calls [audioPlayer.seekToNext]', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + ), + act: (cubit) => cubit.next(), + verify: (_) => verify(audioPlayer.seekToNext).called(1), + ); + }); + + group('previous', () { + blocTest( + 'calls [audioPlayer.seekToPrevious]', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + ), + act: (cubit) => cubit.previous(), + verify: (_) => verify(audioPlayer.seekToPrevious).called(1), + ); + }); + + group('toggleLoop', () { + blocTest( + 'updates [isLoop] to true when it is false', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + ), + act: (cubit) => cubit.toggleLoop(), + expect: () => const [ + MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + isLoop: true, + ), + ], + ); + + blocTest( + 'updates [isLoop] to false when it is true', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + isLoop: true, + ), + act: (cubit) => cubit.toggleLoop(), + expect: () => const [ + MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + ), + ], + ); + + blocTest( + 'calls [audioPlayer.setLoopMode] with [LoopMode.one] when activating', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + ), + act: (cubit) => cubit.toggleLoop(), + verify: (_) => + verify(() => audioPlayer.setLoopMode(LoopMode.one)).called(1), + ); + + blocTest( + 'calls [audioPlayer.setLoopMode] with [LoopMode.off] when deactivating', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + isLoop: true, + ), + act: (cubit) => cubit.toggleLoop(), + verify: (_) => + verify(() => audioPlayer.setLoopMode(LoopMode.off)).called(1), + ); + }); + + group('toggleShuffle', () { + blocTest( + 'updates [isShuffle] to true when it is false', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + ), + act: (cubit) => cubit.toggleShuffle(), + expect: () => const [ + MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + isShuffle: true, + ), + ], + ); + + blocTest( + 'updates [isShuffle] to false when it is true', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + isShuffle: true, + ), + act: (cubit) => cubit.toggleShuffle(), + expect: () => const [ + MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + ), + ], + ); + + blocTest( + 'calls [audioPlayer.setShuffleModeEnabled]', + build: build, + seed: () => const MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + ), + act: (cubit) => cubit.toggleShuffle(), + verify: (_) => + verify(() => audioPlayer.setShuffleModeEnabled(true)).called(1), + ); + }); + }); +} diff --git a/test/music_player/cubit/music_player_state_test.dart b/test/music_player/cubit/music_player_state_test.dart new file mode 100644 index 0000000..0b39abe --- /dev/null +++ b/test/music_player/cubit/music_player_state_test.dart @@ -0,0 +1,51 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:airplane_entertainment_system/music_player/music_player.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:music_repository/music_repository.dart'; + +class _MockMusicTrack extends Mock implements MusicTrack {} + +void main() { + group('MusicPlayerState', () { + group('supports instances comparison', () { + test('with equal instances', () { + final tracks = [_MockMusicTrack()]; + expect( + MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + progress: 10, + isPlaying: true, + isLoop: true, + isShuffle: true, + ), + MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + progress: 10, + isPlaying: true, + isLoop: true, + isShuffle: true, + ), + ); + }); + + test('with non equal instances', () { + final tracks = [_MockMusicTrack()]; + expect( + MusicPlayerState( + tracks: tracks, + currentTrackIndex: 0, + progress: 10, + isPlaying: true, + isLoop: true, + isShuffle: true, + ), + isNot(MusicPlayerState()), + ); + }); + }); + }); +} diff --git a/test/music_player/view/music_player_page_test.dart b/test/music_player/view/music_player_page_test.dart index ca09b7e..869426c 100644 --- a/test/music_player/view/music_player_page_test.dart +++ b/test/music_player/view/music_player_page_test.dart @@ -1,17 +1,50 @@ import 'package:aes_ui/aes_ui.dart'; +import 'package:airplane_entertainment_system/music_player/cubit/music_player_cubit.dart'; import 'package:airplane_entertainment_system/music_player/view/view.dart'; +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:music_repository/music_repository.dart'; import '../../helpers/pump_experience.dart'; +class _MockMusicPlayerCubit extends MockCubit + implements MusicPlayerCubit {} + void main() { + const tracks = [ + MusicTrack(index: 0, title: 'Title0', artist: 'Artist0', path: 'path0'), + MusicTrack(index: 1, title: 'Title1', artist: 'Artist1', path: 'path1'), + ]; + group('MusicPlayerPage', () { + late MusicRepository musicRepository; + late AudioPlayer audioPlayer; + + setUp(() { + musicRepository = MockMusicRepository(); + when(musicRepository.getTracks).thenReturn(tracks); + + audioPlayer = MockAudioPlayer(); + when(() => audioPlayer.audioSource).thenReturn(HlsAudioSource(Uri())); + when(() => audioPlayer.positionStream) + .thenAnswer((_) => const Stream.empty()); + when(() => audioPlayer.playingStream) + .thenAnswer((_) => const Stream.empty()); + when(() => audioPlayer.currentIndexStream) + .thenAnswer((_) => const Stream.empty()); + }); + testWidgets('contains MusicPlayerPage', (tester) async { await tester.pumpApp( const Scaffold( body: MusicPlayerPage(), ), + musicRepository: musicRepository, + audioPlayer: audioPlayer, ); expect(find.byType(MusicMenuView), findsOneWidget); @@ -20,11 +53,15 @@ void main() { testWidgets('when screen size is small, player is shown in a bottom sheet', (tester) async { + when(() => audioPlayer.currentIndexStream) + .thenAnswer((_) => Stream.value(0)); await tester.pumpApp( const Scaffold( body: MusicPlayerPage(), ), layout: AesLayoutData.small, + musicRepository: musicRepository, + audioPlayer: audioPlayer, ); final playerFinder = find.byType(MusicPlayerView); @@ -38,17 +75,113 @@ void main() { expect(playerFinder, findsOneWidget); }); + }); - testWidgets('contains slider and changing it does nothing', (tester) async { - await tester.pumpApp( - const Scaffold( - body: MusicPlayerPage(), + group('MusicPlayerView', () { + late MusicPlayerCubit cubit; + + setUp(() { + cubit = _MockMusicPlayerCubit(); + when(() => cubit.state).thenReturn( + const MusicPlayerState(tracks: tracks), + ); + }); + + Widget subject() { + return BlocProvider.value( + value: cubit, + child: const Material( + child: MusicPlayerView(), ), ); + } + + testWidgets( + 'calls [MusicPlayerCubit.previous] when previous track button is pressed', + (tester) async { + await tester.pumpApp(subject()); + + await tester.tap(find.byIcon(Icons.first_page_rounded)); + verify(cubit.previous).called(1); + }, + ); + + testWidgets( + 'calls [MusicPlayerCubit.next] when next track button is pressed', + (tester) async { + await tester.pumpApp(subject()); - expect(find.byType(Slider), findsOneWidget); - final slider = tester.widget(find.byType(Slider)); - slider.onChanged!(0); + await tester.tap(find.byIcon(Icons.last_page_rounded)); + verify(cubit.next).called(1); + }, + ); + + testWidgets( + 'calls [MusicPlayerCubit.togglePlayPause] when play button is pressed', + (tester) async { + await tester.pumpApp(subject()); + + await tester.tap(find.byIcon(Icons.play_arrow)); + verify(cubit.togglePlayPause).called(1); + }, + ); + + testWidgets( + 'calls [MusicPlayerCubit.seek] when slider is moved', + (tester) async { + await tester.pumpApp(subject()); + + await tester.drag(find.byType(Slider), const Offset(10, 0)); + verify(() => cubit.seek(any())).called(greaterThan(0)); + }, + ); + + testWidgets( + 'calls [MusicPlayerCubit.toggleLoop] when repeat button is pressed', + (tester) async { + await tester.pumpApp(subject()); + + await tester.tap(find.byIcon(Icons.repeat_rounded)); + verify(cubit.toggleLoop).called(1); + }, + ); + + testWidgets( + 'calls [MusicPlayerCubit.toggleShuffle] when shuffle button is pressed', + (tester) async { + await tester.pumpApp(subject()); + + await tester.tap(find.byIcon(Icons.shuffle)); + verify(cubit.toggleShuffle).called(1); + }, + ); + }); + + group('MusicMenuView', () { + late MusicPlayerCubit cubit; + + setUp(() { + cubit = _MockMusicPlayerCubit(); + when(() => cubit.state).thenReturn( + const MusicPlayerState(tracks: tracks), + ); }); + + Widget subject() { + return BlocProvider.value( + value: cubit, + child: const Material(child: MusicMenuView()), + ); + } + + testWidgets( + 'calls [MusicPlayerCubit.playTrack] when track is selected', + (tester) async { + await tester.pumpApp(subject()); + + await tester.tap(find.text('Title0')); + verify(() => cubit.playTrack(tracks[0])).called(1); + }, + ); }); } diff --git a/test/music_player/widgets/music_visualizer_test.dart b/test/music_player/widgets/music_visualizer_test.dart index c8f645f..8db2d31 100644 --- a/test/music_player/widgets/music_visualizer_test.dart +++ b/test/music_player/widgets/music_visualizer_test.dart @@ -27,7 +27,7 @@ void main() { MaterialApp( home: DefaultAssetBundle( bundle: FakeAssetBundle(), - child: const MusicVisualizer(), + child: const MusicVisualizer(isActive: true), ), ), ); @@ -42,5 +42,63 @@ void main() { expect(find.byType(MusicVisualizer), findsOneWidget); }); + + testWidgets('changes animation when activated', (tester) async { + var isActive = false; + late void Function() activate; + + await tester.pumpWidget( + MaterialApp( + home: DefaultAssetBundle( + bundle: FakeAssetBundle(), + child: StatefulBuilder( + builder: (context, setState) { + activate = () => setState(() => isActive = true); + return MusicVisualizer(isActive: isActive); + }, + ), + ), + ), + ); + await tester.pump(const Duration(milliseconds: 50)); + + activate(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 250)); + + final state = tester.state( + find.byType(MusicVisualizer), + ); + expect(state.extensionController.value, 1); + }); + + testWidgets('changes animation when deactivated', (tester) async { + var isActive = true; + late void Function() deactivate; + + await tester.pumpWidget( + MaterialApp( + home: DefaultAssetBundle( + bundle: FakeAssetBundle(), + child: StatefulBuilder( + builder: (context, setState) { + deactivate = () => setState(() => isActive = false); + return MusicVisualizer(isActive: isActive); + }, + ), + ), + ), + ); + await tester.pump(const Duration(milliseconds: 50)); + + deactivate(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 250)); + + final state = tester.state( + find.byType(MusicVisualizer), + ); + expect(state.extensionController.value, 0); + }); }); }