├── test ├── src │ ├── core │ │ └── app_initializer.dart │ ├── datasource │ │ └── http │ │ │ ├── dio_http_config_test.dart │ │ │ └── api │ │ │ └── music_recognition_api_controller_test.dart │ └── features │ │ ├── music_details │ │ └── services │ │ │ └── open_in_player_service_test.dart │ │ └── music_recognition │ │ ├── services │ │ └── audio_recording_service_test.dart │ │ ├── logic │ │ ├── sample_recorder_cubit_test.dart │ │ └── music_recognizer_cubit_test.dart │ │ └── repositories │ │ └── music_recognition_repository_test.dart └── fixtures │ └── music_recognition.dart ├── test_resources ├── file_mocks │ └── test.mp3 └── mocks │ └── recognition_response.json ├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ └── project.pbxproj ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist └── .gitignore ├── demo.gif ├── lib ├── src │ ├── datasource │ │ ├── http │ │ │ ├── typedefs.dart │ │ │ ├── api │ │ │ │ ├── base_api_controller.dart │ │ │ │ └── music_recognition_api_controller.dart │ │ │ ├── exceptions │ │ │ │ └── custom_http_exception.dart │ │ │ └── dio_http_config.dart │ │ ├── models │ │ │ ├── responses │ │ │ │ ├── network_response.dart │ │ │ │ └── recognition_response │ │ │ │ │ ├── apple_music_result │ │ │ │ │ ├── preview.dart │ │ │ │ │ ├── play_params.dart │ │ │ │ │ ├── artwork.dart │ │ │ │ │ └── apple_music_result.dart │ │ │ │ │ ├── spotify_result │ │ │ │ │ ├── image.dart │ │ │ │ │ ├── external_ids.dart │ │ │ │ │ ├── external_urls.dart │ │ │ │ │ ├── artist.dart │ │ │ │ │ ├── album.dart │ │ │ │ │ └── spotify_result.dart │ │ │ │ │ ├── recognition_response.dart │ │ │ │ │ └── recognition_result.dart │ │ │ └── requests │ │ │ │ └── recognize │ │ │ │ └── recognize_request.dart │ │ ├── local │ │ │ ├── converters │ │ │ │ ├── converters.dart │ │ │ │ ├── apple_music_db.dart │ │ │ │ └── spotify_db.dart │ │ │ └── database.dart │ │ └── repositories │ │ │ └── base_repository.dart │ ├── features │ │ ├── music_recognition │ │ │ ├── enums │ │ │ │ └── recognition_failure_reason.dart │ │ │ ├── exceptions │ │ │ │ └── record_failed_exception.dart │ │ │ ├── logic │ │ │ │ ├── music_recognizer │ │ │ │ │ ├── local_recognitions │ │ │ │ │ │ ├── local_recognition_state.dart │ │ │ │ │ │ └── local_recognition.cubit.dart │ │ │ │ │ ├── music_recognizer_state.dart │ │ │ │ │ └── music_recognizer_cubit.dart │ │ │ │ └── sample_recorder │ │ │ │ │ ├── sample_recorder_state.dart │ │ │ │ │ └── sample_recorder_cubit.dart │ │ │ ├── services │ │ │ │ └── audio_recording_service.dart │ │ │ ├── repositories │ │ │ │ ├── music_recognition_repository.dart │ │ │ │ └── local_recognition_repository.dart │ │ │ └── ui │ │ │ │ ├── widgets │ │ │ │ ├── recognition_failed_view.dart │ │ │ │ └── record_button.dart │ │ │ │ └── music_recognition_screen.dart │ │ └── music_details │ │ │ ├── services │ │ │ └── open_in_player_service.dart │ │ │ └── ui │ │ │ └── music_details_screen.dart │ ├── core │ │ ├── environment.dart │ │ ├── theme │ │ │ ├── dimens.dart │ │ │ ├── app_colors.dart │ │ │ └── app_theme.dart │ │ ├── app_initializer.dart │ │ ├── i18n │ │ │ ├── l10n │ │ │ │ └── intl_en.arb │ │ │ ├── intl │ │ │ │ ├── messages_all.dart │ │ │ │ └── messages_en.dart │ │ │ └── l10n.dart │ │ ├── routing │ │ │ └── app_router.dart │ │ ├── application.dart │ │ └── network aware │ │ │ └── network_aware.dart │ └── shared │ │ └── locator.dart ├── main.dart └── generated │ └── assets.gen.dart ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── stevenosse │ │ │ │ │ └── sequence │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── assets └── images │ └── cover-fallback.jpg ├── analysis_options.yaml ├── .gitignore ├── .github ├── workflows │ └── main.yml └── app │ └── checks │ └── action.yml ├── .metadata ├── pubspec.yaml ├── README.md └── z_repo-resources └── data-layer.svg /test/src/core/app_initializer.dart: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_resources/file_mocks/test.mp3: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/demo.gif -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/src/datasource/http/typedefs.dart: -------------------------------------------------------------------------------- 1 | typedef Json = Map; 2 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /assets/images/cover-fallback.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/assets/images/cover-fallback.jpg -------------------------------------------------------------------------------- /lib/src/features/music_recognition/enums/recognition_failure_reason.dart: -------------------------------------------------------------------------------- 1 | enum RecognitionFailureReason { 2 | noMatchFound, 3 | other, 4 | } 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/sequence/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /lib/src/datasource/http/api/base_api_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | abstract class BaseApiController { 4 | final Dio dio; 5 | 6 | BaseApiController({required this.dio}); 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/stevenosse/sequence/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.stevenosse.sequence 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/src/core/environment.dart: -------------------------------------------------------------------------------- 1 | class Environment { 2 | static const String apiBaseUrl = String.fromEnvironment('apiBaseUrl', defaultValue: ''); 3 | 4 | static const String auddApiToken = String.fromEnvironment('AUDD_API_TOKEN'); 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/exceptions/record_failed_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class RecordFailedException extends Equatable with Exception { 4 | @override 5 | List get props => []; 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/network_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'network_response.freezed.dart'; 4 | 5 | @freezed 6 | class NetworkResponse with _$NetworkResponse { 7 | factory NetworkResponse.success(D data) = _Success; 8 | 9 | factory NetworkResponse.error(E error) = _Error; 10 | } 11 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | linter: 4 | rules: 5 | avoid_print: true 6 | prefer_single_quotes: true 7 | use_super_parameters: true 8 | always_declare_return_types: true 9 | avoid_annotating_with_dynamic: true 10 | 11 | analyzer: 12 | exclude: ["**.freezed.dart", "**.g.dart"] 13 | 14 | errors: 15 | invalid_annotation_target: ignore -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/recognition_response/apple_music_result/preview.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'preview.freezed.dart'; 4 | part 'preview.g.dart'; 5 | 6 | @freezed 7 | class Preview with _$Preview { 8 | factory Preview({ 9 | required String url, 10 | }) = _Preview; 11 | 12 | factory Preview.fromJson(Map json) => _$PreviewFromJson(json); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/recognition_response/spotify_result/image.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'image.freezed.dart'; 4 | part 'image.g.dart'; 5 | 6 | @freezed 7 | class Image with _$Image { 8 | factory Image({ 9 | int? height, 10 | int? width, 11 | String? url, 12 | }) = _Image; 13 | 14 | factory Image.fromJson(Map json) => _$ImageFromJson(json); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/recognition_response/spotify_result/external_ids.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'external_ids.freezed.dart'; 4 | part 'external_ids.g.dart'; 5 | 6 | @freezed 7 | class ExternalIds with _$ExternalIds { 8 | factory ExternalIds({ 9 | String? isrc, 10 | }) = _ExternalIds; 11 | 12 | factory ExternalIds.fromJson(Map json) => _$ExternalIdsFromJson(json); 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/recognition_response/spotify_result/external_urls.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'external_urls.freezed.dart'; 4 | part 'external_urls.g.dart'; 5 | 6 | @freezed 7 | class ExternalUrls with _$ExternalUrls { 8 | factory ExternalUrls({ 9 | String? spotify, 10 | }) = _ExternalUrls; 11 | 12 | factory ExternalUrls.fromJson(Map json) => _$ExternalUrlsFromJson(json); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/recognition_response/apple_music_result/play_params.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'play_params.freezed.dart'; 4 | part 'play_params.g.dart'; 5 | 6 | @freezed 7 | class PlayParams with _$PlayParams { 8 | factory PlayParams({ 9 | String? id, 10 | String? kind, 11 | }) = _PlayParams; 12 | 13 | factory PlayParams.fromJson(Map json) => _$PlayParamsFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/features/music_details/services/open_in_player_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:url_launcher/url_launcher_string.dart'; 4 | 5 | class OpenInPlayerService { 6 | Future open(String url) async { 7 | if (await canLaunchUrlString(url)) { 8 | final result = await launchUrlString(url, mode: LaunchMode.externalNonBrowserApplication); 9 | 10 | if (!result) { 11 | log('Failed to open url: $url'); 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/datasource/http/exceptions/custom_http_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class CustomHttpException extends Equatable { 4 | final String code; 5 | final CustomHttpError errorType; 6 | final String details; 7 | 8 | const CustomHttpException({ 9 | required this.code, 10 | required this.details, 11 | required this.errorType, 12 | }); 13 | 14 | @override 15 | List get props => [code, details, errorType]; 16 | } 17 | 18 | enum CustomHttpError { 19 | parsing, 20 | http, 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/logic/music_recognizer/local_recognitions/local_recognition_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'local_recognition_state.freezed.dart'; 4 | 5 | @freezed 6 | class LocalRecognitionState with _$LocalRecognitionState { 7 | factory LocalRecognitionState.idle() = _Idle; 8 | 9 | factory LocalRecognitionState.loading() = _Loading; 10 | 11 | factory LocalRecognitionState.succeeded() = _Succeeded; 12 | 13 | factory LocalRecognitionState.failed() = _Failed; 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/core/theme/dimens.dart: -------------------------------------------------------------------------------- 1 | class Dimens { 2 | static const double halfSpace = 5.0; 3 | static const double space = 10.0; 4 | static const double doubleSpace = 20.0; 5 | static const double tripleSpace = 30.0; 6 | 7 | static const double minPadding = 4.0; 8 | static const double halfPadding = 8.0; 9 | static const double padding = 16.0; 10 | static const double doublePadding = 16.0; 11 | 12 | static const double radius = 12.0; 13 | 14 | static const double recordButtonSize = 150.0; 15 | static const double buttonHeight = 55.0; 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/core/app_initializer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:sequence/src/datasource/local/database.dart'; 4 | 5 | class AppInitializer { 6 | Future preAppRun() async {} 7 | 8 | Future run() async {} 9 | 10 | Future postAppRun(AppDatabase appDatabase) async { 11 | List allRecognitions = []; 12 | 13 | appDatabase 14 | .select(appDatabase.recognitionResultEntityy) 15 | .get() 16 | .then((result) => allRecognitions = result); 17 | 18 | log('message: $allRecognitions'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/music_recognition.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:sequence/src/datasource/models/requests/recognize/recognize_request.dart'; 5 | import 'package:sequence/src/datasource/models/responses/recognition_response/recognition_response.dart'; 6 | 7 | RecognitionResponse defaultRecognitionResponse = RecognitionResponse.fromJson( 8 | jsonDecode(File('test_resources/mocks/recognition_response.json').readAsStringSync()), 9 | ); 10 | 11 | final defaultRequest = RecognizeRequest( 12 | returnOutput: 'apple_music,spotify', 13 | filePath: 'path.mp3', 14 | ); 15 | -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/recognition_response/apple_music_result/artwork.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'artwork.freezed.dart'; 4 | part 'artwork.g.dart'; 5 | 6 | @freezed 7 | class Artwork with _$Artwork { 8 | factory Artwork({ 9 | int? width, 10 | int? height, 11 | String? url, 12 | String? bgColor, 13 | String? textColor1, 14 | String? textColor2, 15 | String? textColor3, 16 | String? textColor4, 17 | }) = _Artwork; 18 | 19 | factory Artwork.fromJson(Map json) => _$ArtworkFromJson(json); 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/recognition_response/spotify_result/artist.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'external_urls.dart'; 4 | 5 | part 'artist.freezed.dart'; 6 | part 'artist.g.dart'; 7 | 8 | @freezed 9 | class Artist with _$Artist { 10 | factory Artist({ 11 | required String name, 12 | required String id, 13 | required String uri, 14 | required String href, 15 | @JsonKey(name: 'external_urls') ExternalUrls? externalUrls, 16 | }) = _Artist; 17 | 18 | factory Artist.fromJson(Map json) => _$ArtistFromJson(json); 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/recognition_response/recognition_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'recognition_result.dart'; 4 | 5 | part 'recognition_response.freezed.dart'; 6 | part 'recognition_response.g.dart'; 7 | 8 | @freezed 9 | class RecognitionResponse with _$RecognitionResponse { 10 | factory RecognitionResponse({ 11 | required String status, 12 | @JsonKey(name: 'result') RecognitionResult? result, 13 | }) = _RecognitionResponse; 14 | 15 | factory RecognitionResponse.fromJson(Map json) => _$RecognitionResponseFromJson(json); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/logic/sample_recorder/sample_recorder_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'sample_recorder_state.freezed.dart'; 4 | 5 | @freezed 6 | class SampleRecorderState with _$SampleRecorderState { 7 | factory SampleRecorderState.idle() = _Idle; 8 | 9 | factory SampleRecorderState.recordPending() = _RecordPending; 10 | 11 | factory SampleRecorderState.recordFailed({ 12 | required Exception exception, 13 | }) = _RecordFailed; 14 | 15 | factory SampleRecorderState.recordSuccessful({ 16 | required String filePath, 17 | }) = _RecordSuccessful; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/services/audio_recording_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:record/record.dart'; 2 | 3 | class AudioRecordingService { 4 | final Record _record; 5 | 6 | AudioRecordingService({Record? record}) : _record = record ?? Record(); 7 | 8 | Future record() async { 9 | // Check and request permission 10 | if (await _record.hasPermission()) { 11 | // Start recording 12 | await _record.start(); 13 | } 14 | } 15 | 16 | Future stopRecording() async { 17 | if (!await _record.isRecording()) { 18 | return null; 19 | } 20 | 21 | return _record.stop(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:sequence/src/core/app_initializer.dart'; 5 | import 'package:sequence/src/core/application.dart'; 6 | import 'package:sequence/src/datasource/local/database.dart'; 7 | 8 | void main() { 9 | WidgetsFlutterBinding.ensureInitialized(); 10 | 11 | final AppInitializer appInitializer = AppInitializer(); 12 | final AppDatabase appDatabase = AppDatabase(); 13 | 14 | runZonedGuarded( 15 | () async { 16 | await appInitializer.preAppRun(); 17 | 18 | runApp(const Application()); 19 | 20 | await appInitializer.postAppRun(appDatabase); 21 | }, 22 | (error, stack) {}, 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.2.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/datasource/http/dio_http_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:sequence/src/core/environment.dart'; 5 | 6 | class DioHttpConfig { 7 | final Dio _dio; 8 | 9 | DioHttpConfig({ 10 | Dio? dioOverride, 11 | }) : _dio = dioOverride ?? _buildDioClient(); 12 | 13 | static Dio _buildDioClient() { 14 | Dio dio = Dio(BaseOptions( 15 | baseUrl: Environment.apiBaseUrl, 16 | )); 17 | 18 | dio.interceptors.addAll([ 19 | LogInterceptor( 20 | responseHeader: false, 21 | responseBody: true, 22 | requestBody: true, 23 | logPrint: (object) => log(object.toString()), 24 | ), 25 | ]); 26 | 27 | return dio; 28 | } 29 | 30 | Dio getDioInstance() => _dio; 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/core/i18n/l10n/intl_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en", 3 | 4 | "musicRecognition_initialActionIndicatorLabel": "Tap to start recognition", 5 | "musicRecognition_failed_noMatch": "No match found", 6 | "musicRecognition_failed_other": "An unexpected error occured. Please try again", 7 | "musicRecognition_failed_tryAgainButtonLabel": "Try again", 8 | "musicRecognition_failed_title": "Recognition failed", 9 | "musicRecognition_recordFailed": "We are unable to record a sample, did you provide microphone access ?", 10 | "musicRecognition_loadingLabel": "Looking for matches...", 11 | 12 | "musicDetails_spotifyButtonlabel": "Open in spotify", 13 | "musicDetails_appleMusicButtonlabel": "Open in Apple Music", 14 | 15 | "developerNotice": "This app is built for learning purposes only." 16 | } -------------------------------------------------------------------------------- /lib/src/core/routing/app_router.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:sequence/src/datasource/models/responses/recognition_response/recognition_result.dart'; 4 | import 'package:sequence/src/features/music_details/services/open_in_player_service.dart'; 5 | import 'package:sequence/src/features/music_details/ui/music_details_screen.dart'; 6 | import 'package:sequence/src/features/music_recognition/ui/music_recognition_screen.dart'; 7 | 8 | part 'app_router.gr.dart'; 9 | 10 | @MaterialAutoRouter( 11 | replaceInRouteName: 'Screen,Route', 12 | routes: [ 13 | AutoRoute(page: MusicRecognitionScreen, initial: true), 14 | AutoRoute(page: MusicDetailsScreen), 15 | ], 16 | ) 17 | class AppRouter extends _$AppRouter {} 18 | -------------------------------------------------------------------------------- /test/src/datasource/http/dio_http_config_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:sequence/src/datasource/http/dio_http_config.dart'; 4 | 5 | void main() { 6 | late DioHttpConfig dioHttpConfig; 7 | 8 | test('Dio instance is built when no Dio instance is provided', () { 9 | final expected = Dio(BaseOptions(baseUrl: '')); 10 | 11 | dioHttpConfig = DioHttpConfig(); 12 | 13 | expect(expected.options.baseUrl, equals(dioHttpConfig.getDioInstance().options.baseUrl)); 14 | }); 15 | 16 | test('Dio instance is overriden', () { 17 | final Dio dio = Dio(BaseOptions(baseUrl: 'https://myapi.com')); 18 | 19 | dioHttpConfig = DioHttpConfig(dioOverride: dio); 20 | expect(dio.options.baseUrl, equals(dioHttpConfig.getDioInstance().options.baseUrl)); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/datasource/http/api/music_recognition_api_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:sequence/src/datasource/http/api/base_api_controller.dart'; 3 | import 'package:sequence/src/datasource/http/typedefs.dart'; 4 | import 'package:sequence/src/datasource/models/requests/recognize/recognize_request.dart'; 5 | 6 | class MusicRecognitionApiController extends BaseApiController { 7 | MusicRecognitionApiController({required super.dio}); 8 | 9 | Future> recognize({ 10 | required RecognizeRequest request, 11 | CancelToken? cancelToken, 12 | }) async { 13 | try { 14 | final Response response = await dio.post( 15 | '/recognize', 16 | options: Options( 17 | contentType: 'multipart/form-data', 18 | ), 19 | data: await request.toFormData(), 20 | ); 21 | 22 | return response; 23 | } on DioError catch (_) { 24 | rethrow; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | .vscode 47 | .fvm 48 | 49 | *.g.dart 50 | *.freezed.dart 51 | *.gr.dart -------------------------------------------------------------------------------- /lib/src/datasource/models/requests/recognize/recognize_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:sequence/src/core/environment.dart'; 4 | 5 | part 'recognize_request.freezed.dart'; 6 | part 'recognize_request.g.dart'; 7 | 8 | @freezed 9 | class RecognizeRequest with _$RecognizeRequest { 10 | @JsonSerializable(explicitToJson: true) 11 | factory RecognizeRequest({ 12 | @JsonKey(name: 'return') required String returnOutput, 13 | required String filePath, 14 | }) = _RecognizeRequest; 15 | 16 | factory RecognizeRequest.fromJson(Map json) => _$RecognizeRequestFromJson(json); 17 | } 18 | 19 | extension RecognizeRequestToFormData on RecognizeRequest { 20 | Future toFormData() async { 21 | return FormData.fromMap({ 22 | ...toJson(), 23 | 'file': await MultipartFile.fromFile(filePath), 24 | 'api_token': Environment.auddApiToken, 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/datasource/local/converters/converters.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:drift/drift.dart'; 4 | 5 | class ListInColumn extends TypeConverter, String> { 6 | @override 7 | List fromSql(String fromDb) { 8 | return (jsonDecode(fromDb) as List).cast(); 9 | } 10 | 11 | @override 12 | String toSql(List value) { 13 | return jsonEncode(value); 14 | } 15 | } 16 | 17 | class ListTInColumn extends TypeConverter, String> { 18 | @override 19 | List fromSql(String fromDb) { 20 | return (jsonDecode(fromDb) as List).cast(); 21 | } 22 | 23 | @override 24 | String toSql(List value) { 25 | return jsonEncode(value); 26 | } 27 | } 28 | 29 | class CustomTypeConverter extends TypeConverter { 30 | @override 31 | T fromSql(String fromDb) { 32 | return (jsonDecode(fromDb) as T); 33 | } 34 | 35 | @override 36 | String toSql(T value) { 37 | return jsonEncode(value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/datasource/repositories/base_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:sequence/src/datasource/http/exceptions/custom_http_exception.dart'; 3 | import 'package:sequence/src/datasource/models/responses/network_response.dart'; 4 | 5 | abstract class BaseRepository { 6 | Future> runApiCall( 7 | {required Future> Function() call}) async { 8 | try { 9 | final response = await call(); 10 | return response; 11 | } on DioError catch (e) { 12 | return NetworkResponse.error(CustomHttpException( 13 | code: e.type.name, 14 | details: e.message, 15 | errorType: CustomHttpError.http, 16 | )); 17 | } catch (e) { 18 | return NetworkResponse.error(CustomHttpException( 19 | code: CustomHttpError.parsing.name, 20 | errorType: CustomHttpError.parsing, 21 | details: e.toString(), 22 | )); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/recognition_response/recognition_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'spotify_result/spotify_result.dart'; 4 | import 'apple_music_result/apple_music_result.dart'; 5 | 6 | part 'recognition_result.freezed.dart'; 7 | part 'recognition_result.g.dart'; 8 | 9 | @freezed 10 | class RecognitionResult with _$RecognitionResult { 11 | @JsonSerializable(explicitToJson: true) 12 | factory RecognitionResult({ 13 | required String artist, 14 | required String title, 15 | String? album, 16 | @JsonKey(name: 'release_date') required String releaseDate, 17 | required String label, 18 | required String timecode, 19 | @JsonKey(name: 'song_link') required String songLink, 20 | @JsonKey(name: 'apple_music') AppleMusicResult? appleMusic, 21 | SpotifyResult? spotify, 22 | }) = _RecognitionResult; 23 | 24 | factory RecognitionResult.fromJson(Map json) => _$RecognitionResultFromJson(json); 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/recognition_response/apple_music_result/apple_music_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'artwork.dart'; 4 | import 'play_params.dart'; 5 | import 'preview.dart'; 6 | 7 | part 'apple_music_result.freezed.dart'; 8 | part 'apple_music_result.g.dart'; 9 | 10 | @freezed 11 | class AppleMusicResult with _$AppleMusicResult { 12 | factory AppleMusicResult({ 13 | @Default([]) List previews, 14 | Artwork? artwork, 15 | required String artistName, 16 | required String url, 17 | int? discNumber, 18 | @Default([]) List genreNames, 19 | int? durationInMillis, 20 | required String releaseDate, 21 | required String name, 22 | String? isrc, 23 | String? albumName, 24 | PlayParams? playParams, 25 | int? trackNumber, 26 | required String composerName, 27 | }) = _AppleMusicResult; 28 | 29 | factory AppleMusicResult.fromJson(Map json) => _$AppleMusicResultFromJson(json); 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Code validation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | validation: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: 📚 Git Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: 📑 Format & Analyze 16 | uses: ./.github/app/checks 17 | 18 | unit-test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: 📚 Git Checkout 22 | uses: actions/checkout@v3 23 | 24 | - uses: subosito/flutter-action@v2 25 | with: 26 | flutter-version: ${{inputs.flutter_version}} 27 | channel: ${{inputs.flutter_channel}} 28 | cache: true 29 | 30 | - name: 🔄 Get dependencies 31 | run: flutter pub get 32 | 33 | - name: 🏗️ Run codegen 34 | run: flutter pub run build_runner build --delete-conflicting-outputs 35 | 36 | - name: 🧪 Run tests 37 | run: flutter test --coverage 38 | 39 | - name: Upload coverage reports to Codecov 40 | uses: codecov/codecov-action@v3 41 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/recognition_response/spotify_result/album.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'artist.dart'; 4 | import 'external_urls.dart'; 5 | import 'image.dart'; 6 | 7 | part 'album.freezed.dart'; 8 | part 'album.g.dart'; 9 | 10 | @freezed 11 | class Album with _$Album { 12 | factory Album({ 13 | required String name, 14 | @Default([]) List artists, 15 | @JsonKey(name: 'album_group') required String albumGroup, 16 | @JsonKey(name: 'album_type') required String albumType, 17 | required String id, 18 | required String uri, 19 | @JsonKey(name: 'available_markets') @Default([]) List availableMarkets, 20 | String? href, 21 | @Default([]) List images, 22 | @JsonKey(name: 'external_urls') ExternalUrls? externalUrls, 23 | @JsonKey(name: 'release_date') required String releaseDate, 24 | @JsonKey(name: 'release_date_precision') required String releaseDatePrecision, 25 | }) = _Album; 26 | 27 | factory Album.fromJson(Map json) => _$AlbumFromJson(json); 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/shared/locator.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | import 'package:sequence/src/datasource/http/api/music_recognition_api_controller.dart'; 3 | import 'package:sequence/src/datasource/http/dio_http_config.dart'; 4 | import 'package:sequence/src/features/music_details/services/open_in_player_service.dart'; 5 | import 'package:sequence/src/features/music_recognition/repositories/local_recognition_repository.dart'; 6 | import 'package:sequence/src/features/music_recognition/repositories/music_recognition_repository.dart'; 7 | import 'package:sequence/src/features/music_recognition/services/audio_recording_service.dart'; 8 | 9 | final GetIt locator = GetIt.instance 10 | ..registerLazySingleton(() => DioHttpConfig()) 11 | ..registerLazySingleton(() => MusicRecognitionApiController( 12 | dio: locator().getDioInstance())) 13 | ..registerLazySingleton(() => MusicRecognitionRepository()) 14 | ..registerLazySingleton(() => AudioRecordingService()) 15 | ..registerLazySingleton(() => OpenInPlayerService()) 16 | ..registerLazySingleton(() => LocalRecognitionRepository()); 17 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/logic/music_recognizer/music_recognizer_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:sequence/src/datasource/models/requests/recognize/recognize_request.dart'; 3 | import 'package:sequence/src/datasource/models/responses/recognition_response/recognition_response.dart'; 4 | import 'package:sequence/src/features/music_recognition/enums/recognition_failure_reason.dart'; 5 | 6 | part 'music_recognizer_state.freezed.dart'; 7 | 8 | @freezed 9 | class MusicRecognizerState with _$MusicRecognizerState { 10 | factory MusicRecognizerState.idle({ 11 | RecognizeRequest? request, 12 | }) = _Idle; 13 | 14 | factory MusicRecognizerState.recognitionLoading({ 15 | required RecognizeRequest request, 16 | }) = _RecognitionLoading; 17 | 18 | factory MusicRecognizerState.recognitionSucceeded({ 19 | required RecognizeRequest request, 20 | required RecognitionResponse response, 21 | }) = _RecognitionSucceeded; 22 | 23 | factory MusicRecognizerState.recognitionFailed({ 24 | required RecognizeRequest request, 25 | required RecognitionFailureReason reason, 26 | }) = _RecognitionFailed; 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/core/application.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:sequence/src/core/i18n/l10n.dart'; 3 | import 'package:sequence/src/core/routing/app_router.dart'; 4 | import 'package:sequence/src/core/theme/app_theme.dart'; 5 | import 'package:flutter_localizations/flutter_localizations.dart'; 6 | 7 | final AppRouter _appRouter = AppRouter(); 8 | 9 | class Application extends StatelessWidget { 10 | const Application({super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return MaterialApp.router( 15 | title: 'Sequence', 16 | routerDelegate: _appRouter.delegate(), 17 | routeInformationProvider: _appRouter.routeInfoProvider(), 18 | routeInformationParser: _appRouter.defaultRouteParser(), 19 | theme: AppTheme.lightTheme, 20 | debugShowCheckedModeBanner: false, 21 | localizationsDelegates: const [ 22 | I18n.delegate, 23 | GlobalMaterialLocalizations.delegate, 24 | GlobalWidgetsLocalizations.delegate, 25 | GlobalCupertinoLocalizations.delegate, 26 | ], 27 | supportedLocales: const [ 28 | Locale('en', 'US'), 29 | ], 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/app/checks/action.yml: -------------------------------------------------------------------------------- 1 | name: App workflow 2 | description: Basic app checks 3 | inputs: 4 | working_directory: 5 | description: Working directory 6 | required: false 7 | default: "." 8 | 9 | flutter_channel: 10 | description: Flutter Channel 11 | required: false 12 | default: "stable" 13 | 14 | shell: 15 | description: "The shell to use for job" 16 | required: false 17 | default: bash 18 | 19 | runs: 20 | using: "composite" 21 | steps: 22 | - uses: subosito/flutter-action@v2 23 | with: 24 | flutter-version: ${{inputs.flutter_version}} 25 | channel: ${{inputs.flutter_channel}} 26 | cache: true 27 | 28 | - working-directory: ${{ inputs.working_directory }} 29 | shell: ${{ inputs.shell }} 30 | run: flutter pub get 31 | 32 | - working-directory: ${{ inputs.working_directory }} 33 | shell: ${{ inputs.shell }} 34 | run: flutter format --line-length 120 lib 35 | 36 | - working-directory: ${{ inputs.working_directory }} 37 | shell: ${{ inputs.shell }} 38 | run: flutter pub run build_runner build --delete-conflicting-outputs 39 | 40 | - working-directory: ${{ inputs.working_directory }} 41 | shell: ${{ inputs.shell }} 42 | run: flutter analyze lib test 43 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/logic/music_recognizer/local_recognitions/local_recognition.cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:sequence/src/datasource/models/responses/recognition_response/recognition_result.dart'; 3 | import 'package:sequence/src/features/music_recognition/logic/music_recognizer/local_recognitions/local_recognition_state.dart'; 4 | import 'package:sequence/src/features/music_recognition/repositories/local_recognition_repository.dart'; 5 | import 'package:sequence/src/shared/locator.dart'; 6 | 7 | class LocalRecognitionCubit extends Cubit { 8 | final LocalRecognitionRepository _localRecognitionRepository; 9 | 10 | LocalRecognitionCubit({ 11 | LocalRecognitionRepository? localRecognitionRepository, 12 | }) : _localRecognitionRepository = 13 | localRecognitionRepository ?? locator(), 14 | super(LocalRecognitionState.idle()); 15 | 16 | void saveRecognition({required RecognitionResult recognitionResponse}) async { 17 | emit(LocalRecognitionState.loading()); 18 | final result = 19 | await _localRecognitionRepository.saveRecognitions(recognitionResponse); 20 | 21 | if(result == 'successful') { 22 | emit(LocalRecognitionState.succeeded()); 23 | } 24 | } 25 | 26 | // void getRecognitions() {} 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/datasource/models/responses/recognition_response/spotify_result/spotify_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'album.dart'; 4 | import 'artist.dart'; 5 | import 'external_ids.dart'; 6 | import 'external_urls.dart'; 7 | 8 | part 'spotify_result.freezed.dart'; 9 | part 'spotify_result.g.dart'; 10 | 11 | @freezed 12 | class SpotifyResult with _$SpotifyResult { 13 | factory SpotifyResult({ 14 | Album? album, 15 | @JsonKey(name: 'external_ids') ExternalIds? externalIds, 16 | @Default(0) int popularity, 17 | @JsonKey(name: 'is_playable') bool? isPlayable, 18 | @JsonKey(name: 'linked_from') String? linkedFrom, 19 | @Default([]) List artists, 20 | @JsonKey(name: 'available_markets') @Default([]) List availableMarkets, 21 | @JsonKey(name: 'disc_number') int? discNumber, 22 | @JsonKey(name: 'duration_ms') int? durationMs, 23 | bool? explicit, 24 | @JsonKey(name: 'external_urls') ExternalUrls? externalUrls, 25 | required String href, 26 | required String id, 27 | required String name, 28 | @JsonKey(name: 'preview_url') required String previewUrl, 29 | @JsonKey(name: 'track_number') int? trackNumber, 30 | required String uri, 31 | }) = _SpotifyResult; 32 | 33 | factory SpotifyResult.fromJson(Map json) => _$SpotifyResultFromJson(json); 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/core/theme/app_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppColors { 4 | final Color primary; 5 | final Color onPrimary; 6 | final Color secondary; 7 | final Color onSecondary; 8 | final Color background; 9 | final Color onBackground; 10 | final Color surface; 11 | final Color onSurface; 12 | final Color error; 13 | final Color onError; 14 | 15 | AppColors.light() 16 | : background = const Color(0xFFFFFFFF), 17 | onBackground = const Color(0xFF000000), 18 | surface = const Color(0xFFFBFAF5), 19 | onSurface = const Color(0xFF000000), 20 | secondary = const Color(0xFFC4C4C4), 21 | onSecondary = const Color(0xFF000000), 22 | primary = const Color(0xff3D5AFE), 23 | onPrimary = const Color(0xFFFBFAF5), 24 | error = const Color(0xFFFF1744), 25 | onError = const Color(0xFFFBFAF5); 26 | 27 | AppColors.dark() 28 | : background = const Color(0xFF11001c), 29 | onBackground = const Color(0xFFFBFAF5), 30 | surface = const Color(0xFF262626), 31 | onSurface = const Color(0xFFFBFAF5), 32 | secondary = const Color(0xFFC4C4C4), 33 | onSecondary = const Color(0xFF000000), 34 | primary = Colors.blueAccent, 35 | onPrimary = const Color(0xFFFBFAF5), 36 | error = const Color(0xFFD50000), 37 | onError = const Color(0xFFFBFAF5); 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/repositories/music_recognition_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:sequence/src/datasource/http/api/music_recognition_api_controller.dart'; 2 | import 'package:sequence/src/datasource/http/exceptions/custom_http_exception.dart'; 3 | import 'package:sequence/src/datasource/models/requests/recognize/recognize_request.dart'; 4 | import 'package:sequence/src/datasource/models/responses/network_response.dart'; 5 | import 'package:sequence/src/datasource/models/responses/recognition_response/recognition_response.dart'; 6 | import 'package:sequence/src/datasource/repositories/base_repository.dart'; 7 | import 'package:sequence/src/shared/locator.dart'; 8 | 9 | class MusicRecognitionRepository extends BaseRepository { 10 | final MusicRecognitionApiController _musicRecognitionApiController; 11 | 12 | MusicRecognitionRepository({ 13 | MusicRecognitionApiController? musicRecognitionApiController, 14 | }) : _musicRecognitionApiController = musicRecognitionApiController ?? locator(); 15 | 16 | Future> recognize(RecognizeRequest request) async { 17 | return runApiCall( 18 | call: () async { 19 | final response = await _musicRecognitionApiController.recognize(request: request); 20 | 21 | return NetworkResponse.success(RecognitionResponse.fromJson(response.data!)); 22 | }, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: b06b8b2710955028a6b562f5aa6fe62941d6febf 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf 17 | base_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf 18 | - platform: android 19 | create_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf 20 | base_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf 21 | - platform: ios 22 | create_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf 23 | base_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf 24 | - platform: web 25 | create_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf 26 | base_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf 27 | - platform: windows 28 | create_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf 29 | base_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf 30 | 31 | # User provided section 32 | 33 | # List of Local paths (relative to this file) that should be 34 | # ignored by the migrate tool. 35 | # 36 | # Files that are not part of the templates will be ignored by default. 37 | unmanaged_files: 38 | - 'lib/main.dart' 39 | - 'ios/Runner.xcodeproj/project.pbxproj' 40 | -------------------------------------------------------------------------------- /test/src/features/music_details/services/open_in_player_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:mocktail/mocktail.dart'; 3 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 4 | import 'package:sequence/src/features/music_details/services/open_in_player_service.dart'; 5 | import 'package:url_launcher/url_launcher_string.dart'; 6 | 7 | // ignore: depend_on_referenced_packages 8 | import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; 9 | 10 | class MockUrlLauncher extends Mock with MockPlatformInterfaceMixin implements UrlLauncherPlatform {} 11 | 12 | void main() { 13 | late OpenInPlayerService openInPlayerService; 14 | late MockUrlLauncher urlLauncher; 15 | 16 | setUp(() { 17 | registerFallbackValue(LaunchMode.externalNonBrowserApplication); 18 | registerFallbackValue(const LaunchOptions()); 19 | 20 | openInPlayerService = OpenInPlayerService(); 21 | urlLauncher = MockUrlLauncher(); 22 | 23 | UrlLauncherPlatform.instance = urlLauncher; 24 | }); 25 | 26 | test('Can open an external url', () async { 27 | const url = 'https://stevenosse.com'; 28 | 29 | when(() => urlLauncher.canLaunch(url)).thenAnswer((_) async => true); 30 | when(() => urlLauncher.launchUrl(url, any())).thenAnswer((_) async => true); 31 | 32 | await openInPlayerService.open(url); 33 | 34 | verify(() => urlLauncher.canLaunch(url)).called(1); 35 | verify(() => urlLauncher.launchUrl(url, any())).called(1); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/logic/music_recognizer/music_recognizer_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:sequence/src/datasource/models/requests/recognize/recognize_request.dart'; 3 | import 'package:sequence/src/features/music_recognition/enums/recognition_failure_reason.dart'; 4 | import 'package:sequence/src/shared/locator.dart'; 5 | 6 | import '../../repositories/music_recognition_repository.dart'; 7 | import 'music_recognizer_state.dart'; 8 | 9 | class MusicRecognizerCubit extends Cubit { 10 | final MusicRecognitionRepository _musicRecognitionRepository; 11 | 12 | MusicRecognizerCubit({ 13 | MusicRecognitionRepository? musicRecognitionRepository, 14 | }) : _musicRecognitionRepository = musicRecognitionRepository ?? locator(), 15 | super(MusicRecognizerState.idle()); 16 | 17 | void recognize({required String filePath}) async { 18 | final request = RecognizeRequest( 19 | returnOutput: 'apple_music,spotify', 20 | filePath: filePath, 21 | ); 22 | 23 | emit(MusicRecognizerState.recognitionLoading(request: request)); 24 | 25 | final response = await _musicRecognitionRepository.recognize(request); 26 | response.when( 27 | success: (data) { 28 | if (data.result != null) { 29 | emit(MusicRecognizerState.recognitionSucceeded(request: request, response: data)); 30 | } else { 31 | emit(MusicRecognizerState.recognitionFailed( 32 | request: request, 33 | reason: RecognitionFailureReason.noMatchFound, 34 | )); 35 | } 36 | }, 37 | error: (error) => emit(MusicRecognizerState.recognitionFailed( 38 | request: request, 39 | reason: RecognitionFailureReason.other, 40 | )), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/core/network aware/network_aware.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: always_declare_return_types 2 | 3 | import 'dart:async'; 4 | import 'dart:developer'; 5 | 6 | import 'package:connectivity_plus/connectivity_plus.dart'; 7 | import 'package:flutter/material.dart'; 8 | 9 | mixin NetworkAwareState on State { 10 | bool _isDisconnected = false; 11 | bool firstCallback = true; 12 | 13 | late StreamSubscription _networkSubscription; 14 | final Connectivity _connectivity = Connectivity(); 15 | 16 | void cancelSubscription() { 17 | try { 18 | _networkSubscription.cancel(); 19 | } catch (e) { 20 | log(e.toString()); 21 | } 22 | } 23 | 24 | @override 25 | void dispose() { 26 | cancelSubscription(); 27 | super.dispose(); 28 | } 29 | 30 | Future initConnectivity() async { 31 | late ConnectivityResult result; 32 | try { 33 | result = await _connectivity.checkConnectivity(); 34 | } on Exception catch (e) { 35 | log(e.toString()); 36 | } 37 | 38 | if (!mounted) { 39 | return Future.value(null); 40 | } 41 | 42 | return _updateConnectionStatus(result); 43 | } 44 | 45 | @override 46 | void initState() { 47 | super.initState(); 48 | initConnectivity(); 49 | _networkSubscription = _connectivity.onConnectivityChanged.listen((result) { 50 | _updateConnectionStatus(result); 51 | }); 52 | } 53 | 54 | void onDisconnected() {} 55 | 56 | void onReconnected() {} 57 | 58 | _updateConnectionStatus(ConnectivityResult result) { 59 | if (result != ConnectivityResult.none) { 60 | if (_isDisconnected) { 61 | onReconnected(); 62 | _isDisconnected = false; 63 | } else { 64 | _isDisconnected = true; 65 | onDisconnected(); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Sequence 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | sequence 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | NSMicrophoneUsageDescription 52 | We need to access to the microphone to record audio file 53 | 54 | 55 | -------------------------------------------------------------------------------- /lib/src/core/theme/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:sequence/src/core/theme/app_colors.dart'; 4 | 5 | class AppTheme { 6 | static ThemeData _buildTheme({required Brightness brightness, required AppColors colors}) { 7 | return ThemeData( 8 | brightness: brightness, 9 | scaffoldBackgroundColor: colors.background, 10 | textTheme: _buildTextTheme(colors: colors), 11 | appBarTheme: AppBarTheme( 12 | elevation: 0, 13 | toolbarHeight: 0, 14 | systemOverlayStyle: SystemUiOverlayStyle( 15 | statusBarColor: Colors.transparent, 16 | statusBarIconBrightness: brightness == Brightness.light ? Brightness.dark : Brightness.light, 17 | ), 18 | backgroundColor: Colors.transparent, 19 | ), 20 | drawerTheme: DrawerThemeData(backgroundColor: colors.background), 21 | cardColor: colors.background, 22 | bottomSheetTheme: BottomSheetThemeData(backgroundColor: colors.background), 23 | dialogTheme: DialogTheme(backgroundColor: colors.background), 24 | primaryColor: colors.primary, 25 | colorScheme: ColorScheme( 26 | background: colors.background, 27 | onBackground: colors.onBackground, 28 | primary: colors.primary, 29 | onPrimary: colors.onPrimary, 30 | surface: colors.surface, 31 | onSurface: colors.onSurface, 32 | secondary: colors.secondary, 33 | onSecondary: colors.onSecondary, 34 | error: colors.error, 35 | brightness: brightness, 36 | onError: colors.onError, 37 | shadow: Colors.black, 38 | ), 39 | ); 40 | } 41 | 42 | static TextTheme _buildTextTheme({required AppColors colors}) { 43 | return const TextTheme(); 44 | } 45 | 46 | static final ThemeData lightTheme = _buildTheme( 47 | brightness: Brightness.light, 48 | colors: AppColors.light(), 49 | ); 50 | 51 | static final ThemeData darkTheme = _buildTheme( 52 | brightness: Brightness.dark, 53 | colors: AppColors.dark(), 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/logic/sample_recorder/sample_recorder_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer'; 3 | 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:sequence/src/features/music_recognition/exceptions/record_failed_exception.dart'; 6 | import 'package:sequence/src/features/music_recognition/logic/sample_recorder/sample_recorder_state.dart'; 7 | import 'package:sequence/src/features/music_recognition/services/audio_recording_service.dart'; 8 | import 'package:sequence/src/shared/locator.dart'; 9 | 10 | class SampleRecorderCubit extends Cubit { 11 | final AudioRecordingService _audioRecordingService; 12 | Timer? _recordTimer; 13 | 14 | static const Duration sampleDuration = Duration(seconds: 5); 15 | 16 | SampleRecorderCubit({ 17 | AudioRecordingService? audioRecordingService, 18 | }) : _audioRecordingService = audioRecordingService ?? locator(), 19 | super(SampleRecorderState.idle()); 20 | 21 | void recordSample() async { 22 | try { 23 | await _startSampleRecording(); 24 | emit(SampleRecorderState.recordPending()); 25 | _recordTimer = Timer(sampleDuration, () => _stopSampleRecording()); 26 | } catch (e) { 27 | log('Record failed', error: e); 28 | } 29 | } 30 | 31 | void reset() { 32 | emit(SampleRecorderState.idle()); 33 | } 34 | 35 | Future _startSampleRecording() async { 36 | await _audioRecordingService.record(); 37 | } 38 | 39 | Future _getRecordedSample() async { 40 | return _audioRecordingService.stopRecording(); 41 | } 42 | 43 | void _stopSampleRecording() async { 44 | final path = await _getRecordedSample(); 45 | if (path != null) { 46 | log('Sample recording ended: path: $path'); 47 | emit(SampleRecorderState.recordSuccessful(filePath: path)); 48 | } else { 49 | emit(SampleRecorderState.recordFailed( 50 | exception: RecordFailedException(), 51 | )); 52 | } 53 | } 54 | 55 | @override 56 | Future close() { 57 | _recordTimer?.cancel(); 58 | return super.close(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/src/features/music_recognition/services/audio_recording_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:mocktail/mocktail.dart'; 3 | import 'package:record/record.dart'; 4 | import 'package:sequence/src/features/music_recognition/services/audio_recording_service.dart'; 5 | 6 | class _MockRecord extends Mock implements Record {} 7 | 8 | void main() { 9 | late Record mockRecord; 10 | late AudioRecordingService audioRecordingService; 11 | 12 | setUp(() { 13 | mockRecord = _MockRecord(); 14 | audioRecordingService = AudioRecordingService(record: mockRecord); 15 | 16 | when(() => mockRecord.start()).thenAnswer((_) async {}); 17 | }); 18 | 19 | test('Can Start Recording when permissions are granted', () async { 20 | when(() => mockRecord.hasPermission()).thenAnswer((_) async => true); 21 | 22 | await audioRecordingService.record(); 23 | 24 | verify(() => mockRecord.start()).called(1); 25 | verify(() => mockRecord.hasPermission()).called(1); 26 | }); 27 | 28 | test('Can\'t Start Recording when permissions are not granted', () async { 29 | when(() => mockRecord.hasPermission()).thenAnswer((_) async => false); 30 | 31 | await audioRecordingService.record(); 32 | 33 | verifyNever(() => mockRecord.start()); 34 | verify(() => mockRecord.hasPermission()).called(1); 35 | }); 36 | 37 | test('Returns path when record pending', () async { 38 | const filePath = 'rsult.mp3'; 39 | 40 | when(() => mockRecord.stop()).thenAnswer((_) async => filePath); 41 | when(() => mockRecord.isRecording()).thenAnswer((_) async => true); 42 | 43 | final String? resultPath = await audioRecordingService.stopRecording(); 44 | 45 | expect(filePath, equals(resultPath)); 46 | }); 47 | 48 | test('Returns null when record pending', () async { 49 | const filePath = 'rsult.mp3'; 50 | 51 | when(() => mockRecord.stop()).thenAnswer((_) async => filePath); 52 | when(() => mockRecord.isRecording()).thenAnswer((_) async => false); 53 | 54 | final String? resultPath = await audioRecordingService.stopRecording(); 55 | 56 | expect(resultPath, isNull); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test/src/datasource/http/api/music_recognition_api_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:http_mock_adapter/http_mock_adapter.dart'; 7 | import 'package:sequence/src/datasource/http/api/music_recognition_api_controller.dart'; 8 | import 'package:sequence/src/datasource/http/typedefs.dart'; 9 | import 'package:sequence/src/datasource/models/requests/recognize/recognize_request.dart'; 10 | 11 | import '../../../../fixtures/music_recognition.dart'; 12 | 13 | void main() { 14 | late Dio dio; 15 | late DioAdapter dioAdapter; 16 | late MusicRecognitionApiController musicRecognitionApiController; 17 | 18 | final request = defaultRequest.copyWith(filePath: 'test_resources/file_mocks/test.mp3'); 19 | 20 | const path = 'https://myapi.com'; 21 | 22 | setUp(() { 23 | dio = Dio(BaseOptions(baseUrl: path)); 24 | dioAdapter = DioAdapter(dio: dio); 25 | musicRecognitionApiController = MusicRecognitionApiController(dio: dio); 26 | }); 27 | 28 | tearDown(() { 29 | dioAdapter.reset(); 30 | }); 31 | 32 | test('Call on /recognize returns valid data', () async { 33 | final jsonResponse = json.decode(File('test_resources/mocks/recognition_response.json').readAsStringSync()); 34 | dioAdapter.onPost( 35 | '/recognize', 36 | data: await request.toFormData(), 37 | (server) => server.reply( 38 | 200, 39 | jsonResponse, 40 | delay: const Duration(seconds: 1), 41 | ), 42 | ); 43 | 44 | final Response response = await musicRecognitionApiController.recognize( 45 | request: request, 46 | ); 47 | 48 | expect(response.data, equals(jsonResponse)); 49 | }); 50 | 51 | test('Call on /recognize throws DioError when call fails', () async { 52 | dioAdapter.onPost( 53 | '/recognize', 54 | data: await request.toFormData(), 55 | (server) => server.reply( 56 | 400, 57 | {'status': 'failed', 'result': null}, 58 | delay: const Duration(seconds: 1), 59 | ), 60 | ); 61 | 62 | final apiCall = musicRecognitionApiController.recognize(request: request); 63 | expectLater(apiCall, throwsA(isA())); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/datasource/local/database.dart: -------------------------------------------------------------------------------- 1 | import 'package:drift/drift.dart'; 2 | import 'dart:io'; 3 | import 'package:drift/native.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | import 'package:path/path.dart' as p; 6 | import 'package:sequence/src/datasource/local/converters/apple_music_db.dart'; 7 | import 'package:sequence/src/datasource/local/converters/spotify_db.dart'; 8 | 9 | part 'database.g.dart'; 10 | 11 | @DataClassName('RecognitionResultEntity') 12 | class RecognitionResultEntityy extends Table { 13 | TextColumn get artist => text()(); 14 | TextColumn get title => text()(); 15 | TextColumn get album => text()(); 16 | TextColumn get releaseDate => text()(); 17 | TextColumn get label => text()(); 18 | TextColumn get timeCode => text()(); 19 | TextColumn get songLink => text()(); 20 | TextColumn get appleMusic => text().map(AppleMusicConverter())(); 21 | TextColumn get spotify => text().map(SpotifyConverter())(); 22 | } 23 | 24 | abstract class RecognitionResultView extends View { 25 | RecognitionResultEntityy get recognitions; 26 | 27 | @override 28 | Query as() => select([recognitions.album]).from(recognitions); 29 | } 30 | 31 | @DriftDatabase( 32 | tables: [ 33 | RecognitionResultEntityy, 34 | ], 35 | views: [ 36 | RecognitionResultView, 37 | ], 38 | ) 39 | class AppDatabase extends _$AppDatabase { 40 | AppDatabase() : super(_openConnection()); 41 | 42 | @override 43 | int get schemaVersion => 1; 44 | 45 | @override 46 | MigrationStrategy get migration { 47 | return MigrationStrategy( 48 | onUpgrade: (m, from, to) async { 49 | await customStatement('PRAGMA foreign_keys = OFF'); 50 | for (var step = from + 1; step <= to; step++) { 51 | switch (step) { 52 | case 2: 53 | //Do something here 54 | break; 55 | } 56 | } 57 | }, 58 | beforeOpen: (details) async { 59 | await customStatement('PRAGMA foreign_keys = ON'); 60 | }, 61 | ); 62 | } 63 | } 64 | 65 | LazyDatabase _openConnection() { 66 | return LazyDatabase( 67 | () async { 68 | final dbFolder = await getApplicationDocumentsDirectory(); 69 | final file = File(p.join(dbFolder.path, 'db.sqlite')); 70 | return NativeDatabase.createInBackground(file); 71 | }, 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/core/i18n/intl/messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names, unnecessary_new 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'dart:async'; 13 | 14 | import 'package:intl/intl.dart'; 15 | import 'package:intl/message_lookup_by_library.dart'; 16 | import 'package:intl/src/intl_helpers.dart'; 17 | 18 | import 'messages_en.dart' as messages_en; 19 | 20 | typedef Future LibraryLoader(); 21 | Map _deferredLibraries = { 22 | 'en': () => new Future.value(null), 23 | }; 24 | 25 | MessageLookupByLibrary? _findExact(String localeName) { 26 | switch (localeName) { 27 | case 'en': 28 | return messages_en.messages; 29 | default: 30 | return null; 31 | } 32 | } 33 | 34 | /// User programs should call this before using [localeName] for messages. 35 | Future initializeMessages(String localeName) async { 36 | var availableLocale = Intl.verifiedLocale( 37 | localeName, (locale) => _deferredLibraries[locale] != null, 38 | onFailure: (_) => null); 39 | if (availableLocale == null) { 40 | return new Future.value(false); 41 | } 42 | var lib = _deferredLibraries[availableLocale]; 43 | await (lib == null ? new Future.value(false) : lib()); 44 | initializeInternalMessageLookup(() => new CompositeMessageLookup()); 45 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 46 | return new Future.value(true); 47 | } 48 | 49 | bool _messagesExistFor(String locale) { 50 | try { 51 | return _findExact(locale) != null; 52 | } catch (e) { 53 | return false; 54 | } 55 | } 56 | 57 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { 58 | var actualLocale = 59 | Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); 60 | if (actualLocale == null) return null; 61 | return _findExact(actualLocale); 62 | } 63 | -------------------------------------------------------------------------------- /test/src/features/music_recognition/logic/sample_recorder_cubit_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | import 'package:sequence/src/features/music_recognition/exceptions/record_failed_exception.dart'; 5 | import 'package:sequence/src/features/music_recognition/logic/sample_recorder/sample_recorder_cubit.dart'; 6 | import 'package:sequence/src/features/music_recognition/logic/sample_recorder/sample_recorder_state.dart'; 7 | import 'package:sequence/src/features/music_recognition/services/audio_recording_service.dart'; 8 | 9 | class _MockAudioRecordingService extends Mock implements AudioRecordingService {} 10 | 11 | void main() { 12 | const String filePath = 'path.mp3'; 13 | 14 | late AudioRecordingService mockAudioRecordingService; 15 | 16 | setUp(() { 17 | mockAudioRecordingService = _MockAudioRecordingService(); 18 | 19 | when(() => mockAudioRecordingService.record()).thenAnswer((invocation) async => {}); 20 | when(() => mockAudioRecordingService.stopRecording()).thenAnswer((invocation) async => filePath); 21 | }); 22 | 23 | blocTest( 24 | 'Final state is [recordSuccessful] when record is launched', 25 | build: () => SampleRecorderCubit(audioRecordingService: mockAudioRecordingService), 26 | act: (bloc) => bloc.recordSample(), 27 | wait: SampleRecorderCubit.sampleDuration, 28 | expect: () => [ 29 | SampleRecorderState.recordPending(), 30 | SampleRecorderState.recordSuccessful(filePath: filePath), 31 | ], 32 | ); 33 | 34 | blocTest( 35 | 'Final state is [idle] when reset is called', 36 | build: () => SampleRecorderCubit(audioRecordingService: mockAudioRecordingService), 37 | act: (bloc) => bloc.reset(), 38 | expect: () => [ 39 | SampleRecorderState.idle(), 40 | ], 41 | ); 42 | 43 | blocTest( 44 | 'Final state is [recordFailed] when record is launched', 45 | setUp: () { 46 | when(() => mockAudioRecordingService.stopRecording()).thenAnswer((_) async => null); 47 | }, 48 | build: () => SampleRecorderCubit(audioRecordingService: mockAudioRecordingService), 49 | act: (bloc) => bloc.recordSample(), 50 | wait: SampleRecorderCubit.sampleDuration, 51 | expect: () => [ 52 | SampleRecorderState.recordPending(), 53 | SampleRecorderState.recordFailed(exception: RecordFailedException()), 54 | ], 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.stevenosse.sequence" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 50 | minSdkVersion 21 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | } 72 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/src/datasource/local/converters/apple_music_db.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:drift/drift.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:sequence/src/datasource/models/responses/recognition_response/apple_music_result/artwork.dart'; 6 | import 'package:sequence/src/datasource/models/responses/recognition_response/apple_music_result/play_params.dart'; 7 | import 'package:sequence/src/datasource/models/responses/recognition_response/apple_music_result/preview.dart'; 8 | import 'package:json_annotation/json_annotation.dart' as j; 9 | 10 | part 'apple_music_db.g.dart'; 11 | 12 | @j.JsonSerializable() 13 | class AppleMusicEntity extends Equatable { 14 | final List previews; 15 | final Artwork? artwork; 16 | final String artistName; 17 | final String url; 18 | final int? discNumber; 19 | final List genreNames; 20 | final int? durationInMillis; 21 | final String releaseDate; 22 | final String name; 23 | final String? isrc; 24 | final String? albumName; 25 | final PlayParams? playParams; 26 | final int? trackNumber; 27 | final String composerName; 28 | 29 | const AppleMusicEntity({ 30 | required this.artistName, 31 | required this.url, 32 | this.discNumber, 33 | required this.genreNames, 34 | this.durationInMillis, 35 | required this.releaseDate, 36 | required this.name, 37 | this.isrc, 38 | this.albumName, 39 | this.playParams, 40 | this.trackNumber, 41 | required this.composerName, 42 | required this.previews, 43 | required this.artwork, 44 | }); 45 | 46 | @override 47 | List get props => [ 48 | artistName, 49 | url, 50 | discNumber, 51 | genreNames, 52 | durationInMillis, 53 | releaseDate, 54 | name, 55 | isrc, 56 | albumName, 57 | playParams, 58 | trackNumber, 59 | composerName, 60 | previews, 61 | artwork, 62 | ]; 63 | 64 | factory AppleMusicEntity.fromJson(Map json) => 65 | _$AppleMusicEntityFromJson(json); 66 | 67 | Map toJson() => _$AppleMusicEntityToJson(this); 68 | } 69 | 70 | class AppleMusicConverter extends TypeConverter { 71 | @override 72 | AppleMusicEntity fromSql(String fromDb) { 73 | return AppleMusicEntity.fromJson(jsonDecode(fromDb) as Map); 74 | } 75 | 76 | @override 77 | String toSql(AppleMusicEntity value) { 78 | return jsonEncode(value.toJson()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/core/i18n/intl/messages_en.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that provides messages for a en locale. All the 3 | // messages from the main program should be duplicated here with the same 4 | // function name. 5 | 6 | // Ignore issues from commonly used lints in this file. 7 | // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new 8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering 9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases 10 | // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes 11 | // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes 12 | 13 | import 'package:intl/intl.dart'; 14 | import 'package:intl/message_lookup_by_library.dart'; 15 | 16 | final messages = new MessageLookup(); 17 | 18 | typedef String MessageIfAbsent(String messageStr, List args); 19 | 20 | class MessageLookup extends MessageLookupByLibrary { 21 | String get localeName => 'en'; 22 | 23 | final messages = _notInlinedMessages(_notInlinedMessages); 24 | static Map _notInlinedMessages(_) => { 25 | "developerNotice": MessageLookupByLibrary.simpleMessage( 26 | "This app is built for learning purposes only."), 27 | "musicDetails_appleMusicButtonlabel": 28 | MessageLookupByLibrary.simpleMessage("Open in Apple Music"), 29 | "musicDetails_spotifyButtonlabel": 30 | MessageLookupByLibrary.simpleMessage("Open in spotify"), 31 | "musicRecognition_failed_noMatch": 32 | MessageLookupByLibrary.simpleMessage("No match found"), 33 | "musicRecognition_failed_other": MessageLookupByLibrary.simpleMessage( 34 | "An unexpected error occured. Please try again"), 35 | "musicRecognition_failed_title": 36 | MessageLookupByLibrary.simpleMessage("Recognition failed"), 37 | "musicRecognition_failed_tryAgainButtonLabel": 38 | MessageLookupByLibrary.simpleMessage("Try again"), 39 | "musicRecognition_initialActionIndicatorLabel": 40 | MessageLookupByLibrary.simpleMessage("Tap to start recognition"), 41 | "musicRecognition_loadingLabel": 42 | MessageLookupByLibrary.simpleMessage("Looking for matches..."), 43 | "musicRecognition_recordFailed": MessageLookupByLibrary.simpleMessage( 44 | "We are unable to record a sample, did you provide microphone access ?") 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/datasource/local/converters/spotify_db.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:drift/drift.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:json_annotation/json_annotation.dart' as j; 6 | import 'package:sequence/src/datasource/models/responses/recognition_response/spotify_result/album.dart'; 7 | import 'package:sequence/src/datasource/models/responses/recognition_response/spotify_result/artist.dart'; 8 | import 'package:sequence/src/datasource/models/responses/recognition_response/spotify_result/external_ids.dart'; 9 | import 'package:sequence/src/datasource/models/responses/recognition_response/spotify_result/external_urls.dart'; 10 | 11 | part 'spotify_db.g.dart'; 12 | 13 | @j.JsonSerializable() 14 | class SpotifyEntity extends Equatable { 15 | final Album? album; 16 | final ExternalIds? externalIds; 17 | final int popularity; 18 | final bool? isPlayable; 19 | final String? linkedFrom; 20 | final List artists; 21 | final List availableMarkets; 22 | final int? discNumber; 23 | final int? durationMs; 24 | final bool? explicit; 25 | final ExternalUrls? externalUrls; 26 | final String href; 27 | final String id; 28 | final String name; 29 | final String previewUrl; 30 | final int? trackNumber; 31 | final String uri; 32 | 33 | const SpotifyEntity({ 34 | this.album, 35 | this.externalIds, 36 | required this.popularity, 37 | this.isPlayable, 38 | this.linkedFrom, 39 | required this.artists, 40 | required this.availableMarkets, 41 | this.discNumber, 42 | this.durationMs, 43 | this.explicit, 44 | this.externalUrls, 45 | required this.href, 46 | required this.id, 47 | required this.name, 48 | required this.previewUrl, 49 | this.trackNumber, 50 | required this.uri, 51 | }); 52 | 53 | @override 54 | List get props => [ 55 | album, 56 | externalIds, 57 | popularity, 58 | isPlayable, 59 | linkedFrom, 60 | artists, 61 | availableMarkets, 62 | discNumber, 63 | durationMs, 64 | explicit, 65 | externalUrls, 66 | href, 67 | id, 68 | name, 69 | previewUrl, 70 | trackNumber, 71 | uri, 72 | ]; 73 | 74 | factory SpotifyEntity.fromJson(Map json) => 75 | _$SpotifyEntityFromJson(json); 76 | 77 | Map toJson() => _$SpotifyEntityToJson(this); 78 | } 79 | 80 | class SpotifyConverter extends TypeConverter { 81 | @override 82 | SpotifyEntity fromSql(String fromDb) { 83 | return SpotifyEntity.fromJson(jsonDecode(fromDb) as Map); 84 | } 85 | 86 | @override 87 | String toSql(SpotifyEntity value) { 88 | return jsonEncode(value.toJson()); 89 | } 90 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: sequence 2 | description: Music recognition application 3 | publish_to: "none" 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: ">=2.19.0 <3.0.0" 8 | 9 | dependencies: 10 | auto_route: ^5.0.4 11 | collection: ^1.17.1 12 | connectivity_plus: ^3.0.3 13 | cupertino_icons: ^1.0.2 14 | dio: ^4.0.6 15 | drift: ^2.5.0 16 | equatable: ^2.0.5 17 | flutter: 18 | sdk: flutter 19 | flutter_bloc: ^8.1.1 20 | flutter_hicons: ^0.0.1 21 | flutter_localizations: 22 | sdk: flutter 23 | freezed_annotation: ^2.2.0 24 | get_it: ^7.2.0 25 | icons_plus: ^4.0.0 26 | intl: ^0.17.0 27 | intl_utils: ^2.7.0 28 | json_annotation: ^4.8.0 29 | lottie: ^2.2.0 30 | path: ^1.8.2 31 | path_provider: ^2.0.12 32 | plugin_platform_interface: ^2.1.3 33 | record: ^4.4.4 34 | sqlite3_flutter_libs: ^0.5.12 35 | url_launcher: ^6.1.9 36 | 37 | dev_dependencies: 38 | auto_route_generator: ^5.0.3 39 | bloc_test: ^9.1.1 40 | build_runner: ^2.3.3 41 | drift_dev: ^2.5.2 42 | flutter_lints: ^2.0.0 43 | flutter_test: 44 | sdk: flutter 45 | freezed: ^2.3.2 46 | http_mock_adapter: ^0.3.3 47 | json_serializable: ^6.6.1 48 | mocktail: ^0.3.0 49 | 50 | dependency_overrides: 51 | collection: ^1.17.1 52 | 53 | flutter: 54 | uses-material-design: true 55 | 56 | # To add assets to your application, add an assets section, like this: 57 | assets: 58 | - assets/animations/ 59 | - assets/images/ 60 | # - images/a_dot_ham.jpeg 61 | # An image asset can refer to one or more resolution-specific "variants", see 62 | # https://flutter.dev/assets-and-images/#resolution-aware 63 | # For details regarding adding assets from package dependencies, see 64 | # https://flutter.dev/assets-and-images/#from-packages 65 | # To add custom fonts to your application, add a fonts section here, 66 | # in this "flutter" section. Each entry in this list should have a 67 | # "family" key with the font family name, and a "fonts" key with a 68 | # list giving the asset and other descriptors for the font. For 69 | # example: 70 | # fonts: 71 | # - family: Schyler 72 | # fonts: 73 | # - asset: fonts/Schyler-Regular.ttf 74 | # - asset: fonts/Schyler-Italic.ttf 75 | # style: italic 76 | # - family: Trajan Pro 77 | # fonts: 78 | # - asset: fonts/TrajanPro.ttf 79 | # - asset: fonts/TrajanPro_Bold.ttf 80 | # weight: 700 81 | # 82 | # For details regarding fonts from package dependencies, 83 | # see https://flutter.dev/custom-fonts/#from-packages 84 | flutter_gen: 85 | output: lib/generated/ 86 | line_length: 120 87 | 88 | fonts: 89 | enabled: false 90 | 91 | flutter_intl: 92 | enabled: true 93 | class_name: I18n 94 | main_locale: en 95 | arb_dir: lib/src/core/i18n/l10n 96 | output_dir: lib/src/core/i18n 97 | -------------------------------------------------------------------------------- /lib/generated/assets.gen.dart: -------------------------------------------------------------------------------- 1 | /// GENERATED CODE - DO NOT MODIFY BY HAND 2 | /// ***************************************************** 3 | /// FlutterGen 4 | /// ***************************************************** 5 | 6 | // coverage:ignore-file 7 | // ignore_for_file: type=lint 8 | // ignore_for_file: directives_ordering,unnecessary_import 9 | 10 | import 'package:flutter/widgets.dart'; 11 | 12 | class $AssetsAnimationsGen { 13 | const $AssetsAnimationsGen(); 14 | 15 | /// File path: assets/animations/waves.json 16 | String get waves => 'assets/animations/waves.json'; 17 | } 18 | 19 | class $AssetsImagesGen { 20 | const $AssetsImagesGen(); 21 | 22 | /// File path: assets/images/cover-fallback.jpg 23 | AssetGenImage get coverFallback => const AssetGenImage('assets/images/cover-fallback.jpg'); 24 | } 25 | 26 | class Assets { 27 | Assets._(); 28 | 29 | static const $AssetsAnimationsGen animations = $AssetsAnimationsGen(); 30 | static const $AssetsImagesGen images = $AssetsImagesGen(); 31 | } 32 | 33 | class AssetGenImage { 34 | const AssetGenImage(this._assetName); 35 | 36 | final String _assetName; 37 | 38 | Image image({ 39 | Key? key, 40 | AssetBundle? bundle, 41 | ImageFrameBuilder? frameBuilder, 42 | ImageErrorWidgetBuilder? errorBuilder, 43 | String? semanticLabel, 44 | bool excludeFromSemantics = false, 45 | double? scale, 46 | double? width, 47 | double? height, 48 | Color? color, 49 | Animation? opacity, 50 | BlendMode? colorBlendMode, 51 | BoxFit? fit, 52 | AlignmentGeometry alignment = Alignment.center, 53 | ImageRepeat repeat = ImageRepeat.noRepeat, 54 | Rect? centerSlice, 55 | bool matchTextDirection = false, 56 | bool gaplessPlayback = false, 57 | bool isAntiAlias = false, 58 | String? package, 59 | FilterQuality filterQuality = FilterQuality.low, 60 | int? cacheWidth, 61 | int? cacheHeight, 62 | }) { 63 | return Image.asset( 64 | _assetName, 65 | key: key, 66 | bundle: bundle, 67 | frameBuilder: frameBuilder, 68 | errorBuilder: errorBuilder, 69 | semanticLabel: semanticLabel, 70 | excludeFromSemantics: excludeFromSemantics, 71 | scale: scale, 72 | width: width, 73 | height: height, 74 | color: color, 75 | opacity: opacity, 76 | colorBlendMode: colorBlendMode, 77 | fit: fit, 78 | alignment: alignment, 79 | repeat: repeat, 80 | centerSlice: centerSlice, 81 | matchTextDirection: matchTextDirection, 82 | gaplessPlayback: gaplessPlayback, 83 | isAntiAlias: isAntiAlias, 84 | package: package, 85 | filterQuality: filterQuality, 86 | cacheWidth: cacheWidth, 87 | cacheHeight: cacheHeight, 88 | ); 89 | } 90 | 91 | String get path => _assetName; 92 | 93 | String get keyName => _assetName; 94 | } 95 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /test/src/features/music_recognition/logic/music_recognizer_cubit_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | import 'package:sequence/src/datasource/http/exceptions/custom_http_exception.dart'; 5 | import 'package:sequence/src/datasource/models/responses/network_response.dart'; 6 | import 'package:sequence/src/features/music_recognition/enums/recognition_failure_reason.dart'; 7 | import 'package:sequence/src/features/music_recognition/logic/music_recognizer/music_recognizer_cubit.dart'; 8 | import 'package:sequence/src/features/music_recognition/logic/music_recognizer/music_recognizer_state.dart'; 9 | import 'package:sequence/src/features/music_recognition/repositories/music_recognition_repository.dart'; 10 | 11 | import '../../../../fixtures/music_recognition.dart'; 12 | 13 | class _MockMusicRecognitionRepository extends Mock implements MusicRecognitionRepository {} 14 | 15 | void main() { 16 | late _MockMusicRecognitionRepository mockMusicRecognitionRepository; 17 | 18 | setUp(() { 19 | mockMusicRecognitionRepository = _MockMusicRecognitionRepository(); 20 | }); 21 | 22 | blocTest( 23 | 'Final state is [recognitionSucceeded] when response is success ', 24 | setUp: () { 25 | when(() => mockMusicRecognitionRepository.recognize(defaultRequest)) 26 | .thenAnswer((_) async => NetworkResponse.success(defaultRecognitionResponse)); 27 | }, 28 | build: () => MusicRecognizerCubit( 29 | musicRecognitionRepository: mockMusicRecognitionRepository, 30 | ), 31 | act: (bloc) => bloc.recognize(filePath: 'path.mp3'), 32 | expect: () => [ 33 | MusicRecognizerState.recognitionLoading(request: defaultRequest), 34 | MusicRecognizerState.recognitionSucceeded(request: defaultRequest, response: defaultRecognitionResponse), 35 | ], 36 | ); 37 | 38 | blocTest( 39 | 'Error is [NO_MATCH_FOUND] when result is null on response ', 40 | setUp: () { 41 | when(() => mockMusicRecognitionRepository.recognize(defaultRequest)) 42 | .thenAnswer((_) async => NetworkResponse.success(defaultRecognitionResponse.copyWith(result: null))); 43 | }, 44 | build: () => MusicRecognizerCubit( 45 | musicRecognitionRepository: mockMusicRecognitionRepository, 46 | ), 47 | act: (bloc) => bloc.recognize(filePath: 'path.mp3'), 48 | expect: () => [ 49 | MusicRecognizerState.recognitionLoading(request: defaultRequest), 50 | MusicRecognizerState.recognitionFailed(request: defaultRequest, reason: RecognitionFailureReason.noMatchFound), 51 | ], 52 | ); 53 | 54 | blocTest( 55 | 'Final state is [recognitionSucceeded] when response is error ', 56 | setUp: () { 57 | when(() => mockMusicRecognitionRepository.recognize(defaultRequest)).thenAnswer((_) async => 58 | NetworkResponse.error(const CustomHttpException(code: '', details: '', errorType: CustomHttpError.parsing))); 59 | }, 60 | build: () => MusicRecognizerCubit( 61 | musicRecognitionRepository: mockMusicRecognitionRepository, 62 | ), 63 | act: (bloc) => bloc.recognize(filePath: 'path.mp3'), 64 | expect: () => [ 65 | MusicRecognizerState.recognitionLoading(request: defaultRequest), 66 | MusicRecognizerState.recognitionFailed(request: defaultRequest, reason: RecognitionFailureReason.other), 67 | ], 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/repositories/local_recognition_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:sequence/src/datasource/local/converters/apple_music_db.dart'; 2 | import 'package:sequence/src/datasource/local/converters/spotify_db.dart'; 3 | import 'package:sequence/src/datasource/local/database.dart'; 4 | import 'package:sequence/src/datasource/models/responses/recognition_response/recognition_result.dart'; 5 | import 'package:sequence/src/datasource/models/responses/recognition_response/spotify_result/album.dart'; 6 | 7 | class LocalRecognitionRepository { 8 | final db = AppDatabase(); 9 | 10 | Future saveRecognitions(RecognitionResult response) async { 11 | try { 12 | await db.into(db.recognitionResultEntityy).insert( 13 | RecognitionResultEntityyCompanion.insert( 14 | artist: response.artist, 15 | title: response.title, 16 | releaseDate: response.releaseDate, 17 | label: response.label, 18 | timeCode: response.timecode, 19 | songLink: response.songLink, 20 | album: response.album ?? '', 21 | appleMusic: AppleMusicEntity( 22 | artistName: response.appleMusic!.artistName, 23 | url: response.appleMusic!.url, 24 | discNumber: response.appleMusic!.discNumber, 25 | durationInMillis: response.appleMusic!.durationInMillis, 26 | isrc: response.appleMusic!.isrc, 27 | albumName: response.appleMusic!.albumName, 28 | playParams: response.appleMusic!.playParams, 29 | trackNumber: response.appleMusic!.trackNumber, 30 | genreNames: response.appleMusic!.genreNames, 31 | releaseDate: response.appleMusic!.releaseDate, 32 | name: response.appleMusic!.name, 33 | composerName: response.appleMusic!.composerName, 34 | previews: response.appleMusic!.previews, 35 | artwork: response.appleMusic!.artwork, 36 | ), 37 | spotify: SpotifyEntity( 38 | popularity: response.spotify!.popularity, 39 | artists: response.spotify!.artists, 40 | availableMarkets: response.spotify!.availableMarkets, 41 | href: response.spotify!.href, 42 | id: response.spotify!.id, 43 | name: response.spotify!.name, 44 | previewUrl: response.spotify!.previewUrl, 45 | uri: response.spotify!.uri, 46 | album: Album( 47 | name: response.spotify!.album!.name, 48 | albumGroup: response.spotify!.album!.albumGroup, 49 | albumType: response.spotify!.album!.albumType, 50 | id: response.spotify!.album!.id, 51 | uri: response.spotify!.album!.id, 52 | releaseDate: response.spotify!.album!.releaseDate, 53 | releaseDatePrecision: 54 | response.spotify!.album!.releaseDatePrecision, 55 | ), 56 | isPlayable: response.spotify!.isPlayable, 57 | linkedFrom: response.spotify!.linkedFrom, 58 | discNumber: response.spotify!.discNumber, 59 | durationMs: response.spotify!.durationMs, 60 | explicit: response.spotify!.explicit, 61 | externalUrls: response.spotify!.externalUrls, 62 | trackNumber: response.spotify!.trackNumber, 63 | externalIds: response.spotify!.externalIds, 64 | ), 65 | ), 66 | ); 67 | return 'Successful'; 68 | } catch (e) { 69 | throw e.toString(); 70 | } 71 | } 72 | 73 | // Future fetchLocalRecognitions() async{} 74 | } 75 | -------------------------------------------------------------------------------- /test/src/features/music_recognition/repositories/music_recognition_repository_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:mocktail/mocktail.dart'; 7 | import 'package:sequence/src/datasource/http/api/music_recognition_api_controller.dart'; 8 | import 'package:sequence/src/datasource/http/exceptions/custom_http_exception.dart'; 9 | import 'package:sequence/src/datasource/models/responses/network_response.dart'; 10 | import 'package:sequence/src/datasource/models/responses/recognition_response/recognition_response.dart'; 11 | import 'package:sequence/src/features/music_recognition/repositories/music_recognition_repository.dart'; 12 | 13 | import '../../../../fixtures/music_recognition.dart'; 14 | 15 | class _MockMusicRecognitionApiController extends Mock implements MusicRecognitionApiController {} 16 | 17 | void main() { 18 | late MusicRecognitionApiController mockMusicRecognitionApiController; 19 | late MusicRecognitionRepository musicRecognitionRepository; 20 | 21 | setUp(() { 22 | mockMusicRecognitionApiController = _MockMusicRecognitionApiController(); 23 | musicRecognitionRepository = MusicRecognitionRepository( 24 | musicRecognitionApiController: mockMusicRecognitionApiController, 25 | ); 26 | }); 27 | 28 | test('Returns [NetworkResponse.error] when API call fails', () async { 29 | when(() => mockMusicRecognitionApiController.recognize(request: defaultRequest)).thenThrow(DioError( 30 | requestOptions: RequestOptions( 31 | path: '/recognize', 32 | ), 33 | type: DioErrorType.other, 34 | )); 35 | 36 | final NetworkResponse response = 37 | await musicRecognitionRepository.recognize(defaultRequest); 38 | 39 | expect( 40 | response, 41 | equals(NetworkResponse.error(CustomHttpException( 42 | code: DioErrorType.other.name, 43 | details: '', 44 | errorType: CustomHttpError.http, 45 | ))), 46 | ); 47 | }); 48 | 49 | test('Returns [NetworkResponse.error] when API call fails with parsing error', () async { 50 | // Should throw 'type 'Null' is not a subtype of type 'String' in type cast' because status cannot be null 51 | when(() => mockMusicRecognitionApiController.recognize(request: defaultRequest)).thenAnswer( 52 | (_) async => Response(requestOptions: RequestOptions(path: '/recognize'), data: { 53 | 'result': '', 54 | }), 55 | ); 56 | 57 | final NetworkResponse response = 58 | await musicRecognitionRepository.recognize(defaultRequest); 59 | 60 | expect( 61 | response, 62 | equals(NetworkResponse.error(CustomHttpException( 63 | code: CustomHttpError.parsing.name, 64 | details: 'type \'Null\' is not a subtype of type \'String\' in type cast', 65 | errorType: CustomHttpError.parsing, 66 | ))), 67 | ); 68 | }); 69 | 70 | test('Returns [NetworkResponse.success] when API Call succeeds', () async { 71 | when(() => mockMusicRecognitionApiController.recognize(request: defaultRequest)).thenAnswer( 72 | (_) async => Response( 73 | requestOptions: RequestOptions(path: '/recognize'), 74 | data: json.decode(File('test_resources/mocks/recognition_response.json').readAsStringSync())), 75 | ); 76 | 77 | final NetworkResponse response = 78 | await musicRecognitionRepository.recognize(defaultRequest); 79 | 80 | expect( 81 | response, 82 | equals(NetworkResponse.success(defaultRecognitionResponse)), 83 | ); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/ui/widgets/recognition_failed_view.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:sequence/src/core/i18n/l10n.dart'; 5 | import 'package:sequence/src/core/theme/dimens.dart'; 6 | import 'package:sequence/src/features/music_recognition/enums/recognition_failure_reason.dart'; 7 | 8 | const _shakeCount = 4; 9 | const _shakeOffset = 10.0; 10 | const _animationDuration = Duration(milliseconds: 500); 11 | 12 | class RecognitionFailedView extends StatefulWidget { 13 | final RecognitionFailureReason reason; 14 | const RecognitionFailedView({ 15 | super.key, 16 | required this.reason, 17 | this.onRetry, 18 | }); 19 | 20 | final VoidCallback? onRetry; 21 | 22 | @override 23 | State createState() => _RecognitionFailedViewState(); 24 | } 25 | 26 | class _RecognitionFailedViewState extends State with SingleTickerProviderStateMixin { 27 | late final AnimationController _animationController; 28 | @override 29 | void initState() { 30 | _animationController = AnimationController(vsync: this, duration: _animationDuration); 31 | _animationController.addStatusListener(_updateStatus); 32 | 33 | WidgetsBinding.instance.addPostFrameCallback((timeStamp) { 34 | // Waiting for the fade animation to finish 35 | Future.delayed(const Duration(milliseconds: 500)).then((value) { 36 | shake(); 37 | }); 38 | }); 39 | super.initState(); 40 | } 41 | 42 | @override 43 | void dispose() { 44 | _animationController.removeStatusListener(_updateStatus); 45 | super.dispose(); 46 | } 47 | 48 | void _updateStatus(AnimationStatus status) { 49 | if (status == AnimationStatus.completed) { 50 | _animationController.reset(); 51 | } 52 | } 53 | 54 | void shake() { 55 | _animationController.forward(); 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return AnimatedBuilder( 61 | animation: _animationController, 62 | builder: (context, _) { 63 | final sineValue = sin(_shakeCount * 2 * pi * _animationController.value); 64 | return Transform.translate( 65 | offset: Offset(sineValue * _shakeOffset, 0), 66 | child: Padding( 67 | padding: const EdgeInsets.all(Dimens.padding), 68 | child: Column( 69 | children: [ 70 | Text( 71 | I18n.of(context).musicRecognition_failed_title, 72 | textAlign: TextAlign.center, 73 | style: 74 | Theme.of(context).textTheme.titleLarge?.copyWith(color: Theme.of(context).colorScheme.onPrimary), 75 | ), 76 | const SizedBox(height: Dimens.space), 77 | Text( 78 | () { 79 | switch (widget.reason) { 80 | case RecognitionFailureReason.noMatchFound: 81 | return I18n.of(context).musicRecognition_failed_noMatch; 82 | case RecognitionFailureReason.other: 83 | return I18n.of(context).musicRecognition_failed_other; 84 | } 85 | }(), 86 | textAlign: TextAlign.center, 87 | style: 88 | Theme.of(context).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onPrimary), 89 | ), 90 | const SizedBox(height: Dimens.doubleSpace), 91 | SizedBox( 92 | height: Dimens.buttonHeight, 93 | width: MediaQuery.of(context).size.width, 94 | child: ElevatedButton( 95 | onPressed: widget.onRetry, 96 | style: ElevatedButton.styleFrom( 97 | backgroundColor: Theme.of(context).colorScheme.onPrimary, 98 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.radius)), 99 | ), 100 | child: Text( 101 | I18n.of(context).musicRecognition_failed_tryAgainButtonLabel, 102 | style: Theme.of(context) 103 | .textTheme 104 | .bodyMedium 105 | ?.copyWith(color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.bold), 106 | ), 107 | ), 108 | ), 109 | ], 110 | ), 111 | ), 112 | ); 113 | }, 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/ui/widgets/record_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hicons/flutter_hicons.dart'; 3 | import 'package:sequence/src/core/i18n/l10n.dart'; 4 | import 'package:sequence/src/core/theme/dimens.dart'; 5 | 6 | class RecordButton extends StatelessWidget { 7 | const RecordButton({ 8 | super.key, 9 | this.onRecordPressed, 10 | }); 11 | 12 | final VoidCallback? onRecordPressed; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Column( 17 | children: [ 18 | SizedBox( 19 | width: Dimens.recordButtonSize * 1.5, 20 | height: Dimens.recordButtonSize * 1.5, 21 | child: Stack( 22 | alignment: Alignment.center, 23 | children: [ 24 | const _Wave(), 25 | InkWell( 26 | customBorder: const CircleBorder(), 27 | onTap: onRecordPressed, 28 | child: Padding( 29 | padding: const EdgeInsets.all(Dimens.padding), 30 | child: Container( 31 | width: Dimens.recordButtonSize, 32 | height: Dimens.recordButtonSize, 33 | decoration: BoxDecoration( 34 | shape: BoxShape.circle, 35 | boxShadow: [ 36 | BoxShadow( 37 | color: Theme.of(context).colorScheme.shadow.withOpacity(.15), 38 | blurRadius: 10.0, 39 | ), 40 | ], 41 | gradient: const RadialGradient( 42 | tileMode: TileMode.decal, 43 | colors: [ 44 | Color(0xff304FFE), 45 | Color(0xff3D5AFE), 46 | Color(0xff536DFE), 47 | ], 48 | ), 49 | ), 50 | child: Icon( 51 | Hicons.microphone_2, 52 | size: Dimens.recordButtonSize / 3, 53 | color: Theme.of(context).colorScheme.onPrimary, 54 | ), 55 | ), 56 | ), 57 | ), 58 | ], 59 | ), 60 | ), 61 | Padding( 62 | padding: const EdgeInsets.all(Dimens.padding), 63 | child: Text( 64 | I18n.of(context).musicRecognition_initialActionIndicatorLabel, 65 | textAlign: TextAlign.center, 66 | style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Theme.of(context).colorScheme.onPrimary), 67 | ), 68 | ), 69 | ], 70 | ); 71 | } 72 | } 73 | 74 | class _Wave extends StatefulWidget { 75 | const _Wave(); 76 | 77 | @override 78 | State<_Wave> createState() => __WaveState(); 79 | } 80 | 81 | class __WaveState extends State<_Wave> with SingleTickerProviderStateMixin { 82 | late AnimationController _animationController; 83 | late Animation _animation; 84 | 85 | static const _scale = .4; 86 | 87 | @override 88 | void initState() { 89 | _animationController = AnimationController( 90 | vsync: this, 91 | duration: const Duration(milliseconds: 1500), 92 | ); 93 | _animation = Tween(begin: 0.0, end: 1.0).animate(_animationController); 94 | 95 | _startAnimation(); 96 | _animationController.addListener(_animationListener); 97 | super.initState(); 98 | } 99 | 100 | @override 101 | void dispose() { 102 | _animationController.removeListener(_animationListener); 103 | _animationController.dispose(); 104 | super.dispose(); 105 | } 106 | 107 | void _startAnimation() { 108 | _animationController.forward(); 109 | } 110 | 111 | void _animationListener() { 112 | if (_animationController.status == AnimationStatus.completed) { 113 | Future.delayed(const Duration(milliseconds: 300)).then((value) { 114 | _animationController.forward(from: 0.0); 115 | }); 116 | } 117 | } 118 | 119 | @override 120 | Widget build(BuildContext context) { 121 | return AnimatedBuilder( 122 | animation: _animation, 123 | builder: (context, child) { 124 | return Opacity( 125 | opacity: (1 - _animation.value).abs().toDouble(), 126 | child: Container( 127 | width: Dimens.recordButtonSize * (_scale + _animation.value), 128 | height: Dimens.recordButtonSize * (_scale + _animation.value), 129 | decoration: BoxDecoration( 130 | color: Theme.of(context).colorScheme.onPrimary.withOpacity(.5), 131 | shape: BoxShape.circle, 132 | ), 133 | ), 134 | ); 135 | }, 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/src/core/i18n/l10n.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | import 'package:flutter/material.dart'; 3 | import 'package:intl/intl.dart'; 4 | import 'intl/messages_all.dart'; 5 | 6 | // ************************************************************************** 7 | // Generator: Flutter Intl IDE plugin 8 | // Made by Localizely 9 | // ************************************************************************** 10 | 11 | // ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars 12 | // ignore_for_file: join_return_with_assignment, prefer_final_in_for_each 13 | // ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes 14 | 15 | class I18n { 16 | I18n(); 17 | 18 | static I18n? _current; 19 | 20 | static I18n get current { 21 | assert(_current != null, 22 | 'No instance of I18n was loaded. Try to initialize the I18n delegate before accessing I18n.current.'); 23 | return _current!; 24 | } 25 | 26 | static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); 27 | 28 | static Future load(Locale locale) { 29 | final name = (locale.countryCode?.isEmpty ?? false) 30 | ? locale.languageCode 31 | : locale.toString(); 32 | final localeName = Intl.canonicalizedLocale(name); 33 | return initializeMessages(localeName).then((_) { 34 | Intl.defaultLocale = localeName; 35 | final instance = I18n(); 36 | I18n._current = instance; 37 | 38 | return instance; 39 | }); 40 | } 41 | 42 | static I18n of(BuildContext context) { 43 | final instance = I18n.maybeOf(context); 44 | assert(instance != null, 45 | 'No instance of I18n present in the widget tree. Did you add I18n.delegate in localizationsDelegates?'); 46 | return instance!; 47 | } 48 | 49 | static I18n? maybeOf(BuildContext context) { 50 | return Localizations.of(context, I18n); 51 | } 52 | 53 | /// `Tap to start recognition` 54 | String get musicRecognition_initialActionIndicatorLabel { 55 | return Intl.message( 56 | 'Tap to start recognition', 57 | name: 'musicRecognition_initialActionIndicatorLabel', 58 | desc: '', 59 | args: [], 60 | ); 61 | } 62 | 63 | /// `No match found` 64 | String get musicRecognition_failed_noMatch { 65 | return Intl.message( 66 | 'No match found', 67 | name: 'musicRecognition_failed_noMatch', 68 | desc: '', 69 | args: [], 70 | ); 71 | } 72 | 73 | /// `An unexpected error occured. Please try again` 74 | String get musicRecognition_failed_other { 75 | return Intl.message( 76 | 'An unexpected error occured. Please try again', 77 | name: 'musicRecognition_failed_other', 78 | desc: '', 79 | args: [], 80 | ); 81 | } 82 | 83 | /// `Try again` 84 | String get musicRecognition_failed_tryAgainButtonLabel { 85 | return Intl.message( 86 | 'Try again', 87 | name: 'musicRecognition_failed_tryAgainButtonLabel', 88 | desc: '', 89 | args: [], 90 | ); 91 | } 92 | 93 | /// `Recognition failed` 94 | String get musicRecognition_failed_title { 95 | return Intl.message( 96 | 'Recognition failed', 97 | name: 'musicRecognition_failed_title', 98 | desc: '', 99 | args: [], 100 | ); 101 | } 102 | 103 | /// `We are unable to record a sample, did you provide microphone access ?` 104 | String get musicRecognition_recordFailed { 105 | return Intl.message( 106 | 'We are unable to record a sample, did you provide microphone access ?', 107 | name: 'musicRecognition_recordFailed', 108 | desc: '', 109 | args: [], 110 | ); 111 | } 112 | 113 | /// `Looking for matches...` 114 | String get musicRecognition_loadingLabel { 115 | return Intl.message( 116 | 'Looking for matches...', 117 | name: 'musicRecognition_loadingLabel', 118 | desc: '', 119 | args: [], 120 | ); 121 | } 122 | 123 | /// `Open in spotify` 124 | String get musicDetails_spotifyButtonlabel { 125 | return Intl.message( 126 | 'Open in spotify', 127 | name: 'musicDetails_spotifyButtonlabel', 128 | desc: '', 129 | args: [], 130 | ); 131 | } 132 | 133 | /// `Open in Apple Music` 134 | String get musicDetails_appleMusicButtonlabel { 135 | return Intl.message( 136 | 'Open in Apple Music', 137 | name: 'musicDetails_appleMusicButtonlabel', 138 | desc: '', 139 | args: [], 140 | ); 141 | } 142 | 143 | /// `This app is built for learning purposes only.` 144 | String get developerNotice { 145 | return Intl.message( 146 | 'This app is built for learning purposes only.', 147 | name: 'developerNotice', 148 | desc: '', 149 | args: [], 150 | ); 151 | } 152 | } 153 | 154 | class AppLocalizationDelegate extends LocalizationsDelegate { 155 | const AppLocalizationDelegate(); 156 | 157 | List get supportedLocales { 158 | return const [ 159 | Locale.fromSubtags(languageCode: 'en'), 160 | ]; 161 | } 162 | 163 | @override 164 | bool isSupported(Locale locale) => _isSupported(locale); 165 | @override 166 | Future load(Locale locale) => I18n.load(locale); 167 | @override 168 | bool shouldReload(AppLocalizationDelegate old) => false; 169 | 170 | bool _isSupported(Locale locale) { 171 | for (var supportedLocale in supportedLocales) { 172 | if (supportedLocale.languageCode == locale.languageCode) { 173 | return true; 174 | } 175 | } 176 | return false; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sequence 2 | 3 |

4 | Music Recognition app using Audd's Music recognition API 5 |

6 | 7 |

8 | 9 | ci 10 | 11 | 12 | 13 | 14 | ci 15 | 16 | 17 | follow on Twitter 19 | 20 | 21 |

22 | 23 | # Content 24 | * [🚧 Getting Started 🚧](#---getting-started---) 25 | + [App Demo:](#app-demo-) 26 | + [Running the app](#running-the-app) 27 | - [Step 1](#step-1) 28 | - [Step 2](#step-2) 29 | - [Step 3](#step-3) 30 | * [Architecture and Folder Structure](#architecture-and-folder-structure) 31 | + [Data layer](#data-layer) 32 | - [Api Controllers](#api-controllers) 33 | - [Repositories](#repositories) 34 | + [Business logic layer](#business-logic-layer) 35 | - [Services](#services) 36 | - [Cubits/BLoCs](#cubits-blocs) 37 | + [Presentation layer](#presentation-layer) 38 | * [Folder structure](#folder-structure) 39 | * [Testing](#testing) 40 | * [Tools](#tools) 41 | 42 | ## 🚧 Getting Started 🚧 43 | 44 | 🏗️ Construction is underway, there may be a lot of elements missing in this README 45 | 46 | ### App Demo: 47 | 48 | ![Application Demo](demo.gif) 49 | 50 | ### Running the app 51 | 52 | Couple steps are required to run this app on your local machine. 53 | 54 | First, get an API Token from Audd by following [this link](https://docs.audd.io/). 55 | 56 | #### Step 1 57 | 58 | An API Token is required to run the app. This API Token is retrieved from environment variables read throught dart define. You'll need to provide is as well as the api base url: 59 | 60 | ```shell 61 | $ flutter run --dart-define apiBaseUrl=https://api.audd.io --dart-define AUDD_API_TOKEN= 62 | ``` 63 | 64 | #### Step 2 65 | 66 | Install dependencies: 67 | 68 | ```shell 69 | $ flutter pub get 70 | ``` 71 | 72 | #### Step 3 73 | 74 | Run code generation: 75 | 76 | ```shell 77 | $ flutter pub run build_runner build --delete-conflicting-outputs 78 | ``` 79 | 80 | ## Architecture and Folder Structure 81 | 82 | This project uses the BLoC Architecture. Learn more on [https://bloclibrary.dev](https://bloclibrary.dev/#/architecture) 83 | 84 | ### Data layer 85 | 86 | This layer is responsible of manipulating data from one or more sources 87 | 88 | ![Data layer](./z_repo-resources/data-layer.svg) 89 | 90 | #### Api Controllers 91 | 92 | Are responsible for API calls. They all extend the `BaseApiController` class. Are not responsible for data processing. 93 | 94 | ```dart 95 | class UserApiController extends BaseApiController { 96 | UserApiController({required super.dio}); 97 | 98 | Future> recognize({ 99 | required Request request, 100 | CancelToken? cancelToken, 101 | }) async { 102 | try { 103 | final Response response = await dio.post('/endpoint', data: await request.toJson()); 104 | 105 | return response; 106 | } on DioError catch (_) { 107 | rethrow; 108 | } 109 | } 110 | } 111 | 112 | ``` 113 | 114 | #### Repositories 115 | 116 | They can aggregate multiple data sources (eg. Multiple API Controllers). They all extend be `BaseRepository` class which contains the `runApiCall` method that has necessary logi cfor errors handling. 117 | 118 | ```dart 119 | class TestRepository extends BaseRepository { 120 | final MyApiController _myApiController; 121 | final MySecondApiController _mySecondApiController; 122 | 123 | TestRepository({ 124 | MyApiController? myApiController, 125 | MySecondApiController? mySecondApiController, 126 | }) : _myApiController = myApiController ?? locator(), 127 | _mySecondApiController = mySecondApiController ?? locator(); 128 | 129 | Future> test(MyRequest request) async { 130 | return runApiCall( 131 | call: () async { 132 | final response = await _myApiController.test(request: request); 133 | 134 | return NetworkResponse.success(TestResponse.fromJson(response.data!)); 135 | }, 136 | ); 137 | } 138 | } 139 | ``` 140 | 141 | ### Business logic layer 142 | 143 | > The business logic layer's responsibility is to respond to input from the presentation layer with new states. This layer can depend on one or more repositories to retrieve data needed to build up the application state. 144 | 145 | From [https://bloclibrary.dev](https://bloclibrary.dev/#/architecture?id=business-logic-layer) 146 | 147 | This layer hosts our blocks, cubits and services. 148 | 149 | > #### Services 150 | > 151 | > They are used to store any logic that's not related to a UI State 152 | 153 | #### Cubits/BLoCs 154 | > 155 | > They are used to store any logic that can result to a UI State change 156 | 157 | ### Presentation layer 158 | 159 | This layer is the one the user interacts with. It renders itself base on one or more BLoC/Cubit's state 160 | 161 | ### Folder structure 162 | 163 | Here's how our folder structure look like: 164 | 165 | ```markdown 166 | lib/ 167 | ├── generated/ 168 | └── src/ 169 | ├── core/ 170 | │ ├── i18n/ 171 | │ ├── routing/ 172 | │ ├── theme/ 173 | │ ├── app_initialiser.dart 174 | │ ├── application.dart 175 | │ └── environment.dart 176 | ├── datasource/ 177 | │ ├── http/ 178 | │ ├── models/ 179 | │ └── repositories/ 180 | │ └── base_repository.dart 181 | ├── features/ 182 | │ ├── music_recognition/ 183 | │ │ ├── enums/ 184 | │ │ ├── exceptions/ 185 | │ │ ├── logic/ 186 | │ │ ├── repositories/ 187 | │ │ ├── services/ 188 | │ │ └── ui/ 189 | │ └── music_details/ 190 | └── main.dart 191 | ``` 192 | 193 | ## Testing 194 | 195 | This repo uses couple of testing libs : 196 | 197 | - [bloc_test](https://pub.dev/packages/bloc_test): A Dart package that makes testing blocs and cubits easy 198 | - [mocktail](https://pub.dev/packages/mocktail): A Dart mock library which simplifies mocking with null safety support and no manual mocks or code generation. 199 | - [http_mock_adapter](https://pub.dev/packages/http_mock_adapter): A simple to use mocking package for Dio intended to be used in tests. It provides various types and methods to declaratively mock request-response communication. 200 | 201 | Usage examples are available in the `test` dir. 202 | 203 | ## Tools 204 | 205 | - [fvm](fvm.app): used for flutter version management 206 | - [flutter_gen](https://pub.dev/packages/flutter_gen): Used to generated assets 207 | -------------------------------------------------------------------------------- /lib/src/features/music_recognition/ui/music_recognition_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:lottie/lottie.dart'; 6 | import 'package:sequence/generated/assets.gen.dart'; 7 | import 'package:sequence/src/core/i18n/l10n.dart'; 8 | import 'package:sequence/src/core/routing/app_router.dart'; 9 | import 'package:sequence/src/core/theme/dimens.dart'; 10 | import 'package:sequence/src/features/music_recognition/logic/music_recognizer/local_recognitions/local_recognition.cubit.dart'; 11 | import 'package:sequence/src/features/music_recognition/logic/music_recognizer/music_recognizer_cubit.dart'; 12 | import 'package:sequence/src/features/music_recognition/logic/music_recognizer/music_recognizer_state.dart'; 13 | import 'package:sequence/src/features/music_recognition/logic/sample_recorder/sample_recorder_cubit.dart'; 14 | import 'package:sequence/src/features/music_recognition/logic/sample_recorder/sample_recorder_state.dart'; 15 | import 'package:sequence/src/features/music_recognition/ui/widgets/recognition_failed_view.dart'; 16 | import 'package:sequence/src/features/music_recognition/ui/widgets/record_button.dart'; 17 | 18 | class MusicRecognitionScreen extends StatefulWidget 19 | implements AutoRouteWrapper { 20 | const MusicRecognitionScreen({super.key}); 21 | 22 | @override 23 | State createState() => _MusicRecognitionScreenState(); 24 | 25 | @override 26 | Widget wrappedRoute(BuildContext context) { 27 | return MultiBlocProvider( 28 | providers: [ 29 | BlocProvider(create: (_) => SampleRecorderCubit()), 30 | BlocProvider(create: (_) => MusicRecognizerCubit()), 31 | BlocProvider(create: (_) => LocalRecognitionCubit()), 32 | ], 33 | child: this, 34 | ); 35 | } 36 | } 37 | 38 | class _MusicRecognitionScreenState extends State { 39 | @override 40 | Widget build(BuildContext context) { 41 | return Scaffold( 42 | appBar: AppBar( 43 | toolbarHeight: 0, 44 | systemOverlayStyle: SystemUiOverlayStyle( 45 | statusBarIconBrightness: Brightness.light, 46 | statusBarColor: Theme.of(context).colorScheme.primary, 47 | ), 48 | ), 49 | backgroundColor: Theme.of(context).colorScheme.primary, 50 | body: BlocListener( 51 | listener: (context, state) { 52 | state.whenOrNull( 53 | recognitionSucceeded: (request, response) { 54 | context.read().reset(); 55 | context 56 | .read() 57 | .saveRecognition(recognitionResponse: response.result!); 58 | context.router 59 | .push(MusicDetailsRoute(recognitionResult: response.result!)); 60 | }, 61 | ); 62 | }, 63 | child: BlocConsumer( 64 | listener: (context, state) { 65 | state.whenOrNull( 66 | recordSuccessful: (filePath) { 67 | context 68 | .read() 69 | .recognize(filePath: filePath); 70 | }, 71 | recordFailed: (exception) { 72 | ScaffoldMessenger.of(context).showSnackBar( 73 | SnackBar( 74 | backgroundColor: Theme.of(context).colorScheme.error, 75 | content: Text( 76 | I18n.of(context).musicRecognition_recordFailed, 77 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( 78 | color: Theme.of(context).colorScheme.onError), 79 | ), 80 | ), 81 | ); 82 | }, 83 | ); 84 | }, 85 | builder: (context, state) { 86 | return SizedBox( 87 | width: MediaQuery.of(context).size.width, 88 | height: MediaQuery.of(context).size.height, 89 | child: Column( 90 | crossAxisAlignment: CrossAxisAlignment.center, 91 | children: [ 92 | const Spacer(), 93 | AnimatedCrossFade( 94 | firstChild: const _RecordView(), 95 | secondChild: const _RecognitionStatus(), 96 | crossFadeState: state.maybeWhen( 97 | recordSuccessful: (filePath) => CrossFadeState.showSecond, 98 | orElse: () => CrossFadeState.showFirst, 99 | ), 100 | duration: const Duration(milliseconds: 500), 101 | excludeBottomFocus: false, 102 | sizeCurve: Curves.decelerate, 103 | ), 104 | const Spacer(), 105 | Text( 106 | I18n.of(context).developerNotice, 107 | style: Theme.of(context).textTheme.titleSmall?.copyWith( 108 | color: Theme.of(context).colorScheme.onPrimary, 109 | fontWeight: FontWeight.normal, 110 | fontSize: 12.0), 111 | ), 112 | const SizedBox(height: Dimens.doubleSpace), 113 | ], 114 | ), 115 | ); 116 | }, 117 | ), 118 | ), 119 | ); 120 | } 121 | } 122 | 123 | class _RecordView extends StatelessWidget { 124 | const _RecordView(); 125 | 126 | @override 127 | Widget build(BuildContext context) { 128 | return BlocBuilder( 129 | builder: (context, state) { 130 | return state.maybeWhen( 131 | recordSuccessful: (filePath) => const SizedBox(), 132 | orElse: () => AnimatedCrossFade( 133 | firstChild: RecordButton( 134 | onRecordPressed: () => 135 | context.read().recordSample(), 136 | ), 137 | secondChild: Lottie.asset(Assets.animations.waves), 138 | crossFadeState: state.maybeWhen( 139 | recordPending: () => CrossFadeState.showSecond, 140 | orElse: () => CrossFadeState.showFirst, 141 | ), 142 | duration: const Duration(milliseconds: 500), 143 | ), 144 | ); 145 | }, 146 | ); 147 | } 148 | } 149 | 150 | class _RecognitionStatus extends StatelessWidget { 151 | const _RecognitionStatus(); 152 | 153 | @override 154 | Widget build(BuildContext context) { 155 | return BlocBuilder( 156 | builder: (context, state) { 157 | return state.maybeWhen( 158 | recognitionLoading: (request) => Text( 159 | I18n.of(context).musicRecognition_loadingLabel, 160 | style: Theme.of(context) 161 | .textTheme 162 | .titleSmall 163 | ?.copyWith(color: Theme.of(context).colorScheme.onPrimary), 164 | ), 165 | recognitionFailed: (request, reason) { 166 | return RecognitionFailedView( 167 | reason: reason, 168 | onRetry: () => context.read().recordSample(), 169 | ); 170 | }, 171 | orElse: () => const SizedBox(), 172 | ); 173 | }, 174 | ); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /lib/src/features/music_details/ui/music_details_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:collection/collection.dart'; 5 | import 'package:icons_plus/icons_plus.dart'; 6 | import 'package:sequence/generated/assets.gen.dart'; 7 | import 'package:sequence/src/core/i18n/l10n.dart'; 8 | import 'package:sequence/src/core/theme/dimens.dart'; 9 | 10 | import 'package:sequence/src/datasource/models/responses/recognition_response/recognition_result.dart'; 11 | import 'package:sequence/src/features/music_details/services/open_in_player_service.dart'; 12 | import 'package:sequence/src/shared/locator.dart'; 13 | 14 | class MusicDetailsScreen extends StatelessWidget { 15 | MusicDetailsScreen({ 16 | super.key, 17 | required this.recognitionResult, 18 | OpenInPlayerService? openInPlayerService, 19 | }) : _openInPlayerService = 20 | openInPlayerService ?? locator(); 21 | 22 | final RecognitionResult recognitionResult; 23 | final OpenInPlayerService _openInPlayerService; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | final songCoverImagePath = 28 | recognitionResult.spotify?.album?.images.firstOrNull?.url ?? 29 | recognitionResult.appleMusic?.artwork?.url; 30 | 31 | return Scaffold( 32 | body: SizedBox( 33 | width: MediaQuery.of(context).size.width, 34 | height: MediaQuery.of(context).size.height, 35 | child: Stack( 36 | children: [ 37 | _AlbumCover(url: songCoverImagePath), 38 | const _OverlayGradient(), 39 | CustomScrollView( 40 | slivers: [ 41 | SliverAppBar( 42 | expandedHeight: MediaQuery.of(context).size.height * .8, 43 | flexibleSpace: FlexibleSpaceBar( 44 | titlePadding: const EdgeInsets.all(Dimens.padding), 45 | title: Column( 46 | crossAxisAlignment: CrossAxisAlignment.start, 47 | mainAxisSize: MainAxisSize.min, 48 | children: [ 49 | Text(recognitionResult.title, textScaleFactor: 1), 50 | Text( 51 | recognitionResult.artist, 52 | style: Theme.of(context) 53 | .textTheme 54 | .labelMedium 55 | ?.copyWith( 56 | color: 57 | Theme.of(context).colorScheme.onPrimary), 58 | ), 59 | if (recognitionResult.album != null) ...[ 60 | const SizedBox(height: Dimens.halfSpace), 61 | Text( 62 | recognitionResult.album!, 63 | style: Theme.of(context) 64 | .textTheme 65 | .labelSmall 66 | ?.copyWith( 67 | color: 68 | Theme.of(context).colorScheme.onPrimary, 69 | fontSize: 8.0), 70 | ), 71 | ] 72 | ], 73 | ), 74 | ), 75 | ), 76 | const SliverToBoxAdapter(child: SizedBox(height: Dimens.space)), 77 | SliverToBoxAdapter( 78 | child: Wrap( 79 | spacing: Dimens.space, 80 | runSpacing: Dimens.space, 81 | alignment: WrapAlignment.center, 82 | children: [ 83 | if (recognitionResult.spotify != null) 84 | _SpotifyButton( 85 | onPressed: () { 86 | final trackUrl = recognitionResult 87 | .spotify!.externalUrls?.spotify; 88 | if (trackUrl != null) { 89 | _openInPlayerService.open(trackUrl); 90 | } else { 91 | log('Failed to open spotify music: url is null'); 92 | } 93 | }, 94 | ), 95 | if (recognitionResult.appleMusic != null) 96 | _AppleMusicButton( 97 | onPressed: () { 98 | final trackUrl = recognitionResult.appleMusic!.url; 99 | 100 | _openInPlayerService.open(trackUrl); 101 | }, 102 | ), 103 | ], 104 | ), 105 | ) 106 | ], 107 | ) 108 | ], 109 | ), 110 | ), 111 | ); 112 | } 113 | } 114 | 115 | class _AlbumCover extends StatelessWidget { 116 | const _AlbumCover({this.url}); 117 | 118 | final String? url; 119 | 120 | @override 121 | Widget build(BuildContext context) { 122 | return Container( 123 | width: MediaQuery.of(context).size.width, 124 | height: MediaQuery.of(context).size.height, 125 | decoration: BoxDecoration( 126 | image: DecorationImage( 127 | image: (url == null 128 | ? AssetImage(Assets.images.coverFallback.path) 129 | : NetworkImage(url!)) as ImageProvider, 130 | fit: BoxFit.cover, 131 | ), 132 | ), 133 | ); 134 | } 135 | } 136 | 137 | class _OverlayGradient extends StatelessWidget { 138 | const _OverlayGradient(); 139 | 140 | @override 141 | Widget build(BuildContext context) { 142 | return Container( 143 | width: MediaQuery.of(context).size.width, 144 | height: MediaQuery.of(context).size.height, 145 | decoration: const BoxDecoration( 146 | gradient: LinearGradient( 147 | begin: Alignment.bottomCenter, 148 | end: Alignment.topCenter, 149 | colors: [ 150 | Colors.black, 151 | Colors.black54, 152 | Colors.white54, 153 | ], 154 | ), 155 | ), 156 | ); 157 | } 158 | } 159 | 160 | class _SpotifyButton extends StatelessWidget { 161 | const _SpotifyButton({this.onPressed}); 162 | 163 | final VoidCallback? onPressed; 164 | 165 | @override 166 | Widget build(BuildContext context) { 167 | return SizedBox( 168 | height: Dimens.buttonHeight, 169 | child: ElevatedButton.icon( 170 | onPressed: onPressed, 171 | style: ElevatedButton.styleFrom( 172 | backgroundColor: Colors.white24, 173 | shape: const StadiumBorder(), 174 | ), 175 | icon: Logo(Logos.spotify, size: Dimens.doubleSpace), 176 | label: Text(I18n.of(context).musicDetails_spotifyButtonlabel), 177 | ), 178 | ); 179 | } 180 | } 181 | 182 | class _AppleMusicButton extends StatelessWidget { 183 | const _AppleMusicButton({this.onPressed}); 184 | 185 | final VoidCallback? onPressed; 186 | 187 | @override 188 | Widget build(BuildContext context) { 189 | return SizedBox( 190 | height: Dimens.buttonHeight, 191 | child: ElevatedButton.icon( 192 | onPressed: onPressed, 193 | style: ElevatedButton.styleFrom( 194 | backgroundColor: Colors.white24, 195 | shape: const StadiumBorder(), 196 | ), 197 | icon: Logo(Logos.apple_music, size: Dimens.doubleSpace), 198 | label: Text(I18n.of(context).musicDetails_appleMusicButtonlabel), 199 | ), 200 | ); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /z_repo-resources/data-layer.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | API Controller 23 | 1 24 | 25 | 26 | 27 | 28 | 29 | API Controller 30 | 2 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Repository 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /test_resources/mocks/recognition_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "success", 3 | "result": { 4 | "artist": "Captaine Roshi", 5 | "title": "Croc aiguisé", 6 | "album": "Larosh", 7 | "release_date": "2022-04-22", 8 | "label": "UMG - Universal Music Division Virgin Records", 9 | "timecode": "00:18", 10 | "song_link": "https://lis.tn/CrocAiguis%C3%A9", 11 | "apple_music": { 12 | "previews": [ 13 | { 14 | "url": "https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview122/v4/78/4c/09/784c09ed-f17c-ebbc-7cb4-4a5ab3182e0a/mzaf_9200352403057982704.plus.aac.p.m4a" 15 | } 16 | ], 17 | "artwork": { 18 | "width": 3000, 19 | "height": 3000, 20 | "url": "https://is3-ssl.mzstatic.com/image/thumb/Music126/v4/36/4f/37/364f37b1-db95-b3ac-cbe8-956403458f95/21UM1IM51063.rgb.jpg/{w}x{h}bb.jpg", 21 | "bgColor": "110f10", 22 | "textColor1": "faf7ee", 23 | "textColor2": "43cbb7", 24 | "textColor3": "ccc9c2", 25 | "textColor4": "39a595" 26 | }, 27 | "artistName": "Captaine Roshi", 28 | "url": "https://music.apple.com/us/album/croc-aiguis%C3%A9/1613928941?app=music&at=1000l33QU&i=1613929188&mt=1", 29 | "discNumber": 1, 30 | "genreNames": ["Hip-Hop/Rap", "Music"], 31 | "durationInMillis": 216400, 32 | "releaseDate": "2022-04-22", 33 | "name": "Croc aiguisé", 34 | "isrc": "FRUM72200454", 35 | "albumName": "Larosh", 36 | "playParams": { 37 | "id": "1613929188", 38 | "kind": "song" 39 | }, 40 | "trackNumber": 16, 41 | "composerName": "Jonayd & Jead" 42 | }, 43 | "spotify": { 44 | "album": { 45 | "name": "Larosh", 46 | "artists": [ 47 | { 48 | "name": "Captaine Roshi", 49 | "id": "4bDcCV0zjPsVs2GxtduYry", 50 | "uri": "spotify:artist:4bDcCV0zjPsVs2GxtduYry", 51 | "href": "https://api.spotify.com/v1/artists/4bDcCV0zjPsVs2GxtduYry", 52 | "external_urls": { 53 | "spotify": "https://open.spotify.com/artist/4bDcCV0zjPsVs2GxtduYry" 54 | } 55 | } 56 | ], 57 | "album_group": "", 58 | "album_type": "album", 59 | "id": "79lzvkA5KtvBmKsoPEFJxH", 60 | "uri": "spotify:album:79lzvkA5KtvBmKsoPEFJxH", 61 | "available_markets": [ 62 | "AD", 63 | "AE", 64 | "AG", 65 | "AL", 66 | "AM", 67 | "AO", 68 | "AR", 69 | "AT", 70 | "AU", 71 | "AZ", 72 | "BA", 73 | "BB", 74 | "BD", 75 | "BE", 76 | "BF", 77 | "BG", 78 | "BH", 79 | "BI", 80 | "BJ", 81 | "BN", 82 | "BO", 83 | "BR", 84 | "BS", 85 | "BT", 86 | "BW", 87 | "BZ", 88 | "CA", 89 | "CD", 90 | "CG", 91 | "CH", 92 | "CI", 93 | "CL", 94 | "CM", 95 | "CO", 96 | "CR", 97 | "CV", 98 | "CW", 99 | "CY", 100 | "CZ", 101 | "DE", 102 | "DJ", 103 | "DK", 104 | "DM", 105 | "DO", 106 | "DZ", 107 | "EC", 108 | "EE", 109 | "EG", 110 | "ES", 111 | "ET", 112 | "FI", 113 | "FJ", 114 | "FM", 115 | "FR", 116 | "GA", 117 | "GB", 118 | "GD", 119 | "GE", 120 | "GH", 121 | "GM", 122 | "GN", 123 | "GQ", 124 | "GR", 125 | "GT", 126 | "GW", 127 | "GY", 128 | "HK", 129 | "HN", 130 | "HR", 131 | "HT", 132 | "HU", 133 | "ID", 134 | "IE", 135 | "IL", 136 | "IN", 137 | "IQ", 138 | "IS", 139 | "IT", 140 | "JM", 141 | "JO", 142 | "JP", 143 | "KE", 144 | "KG", 145 | "KH", 146 | "KI", 147 | "KM", 148 | "KN", 149 | "KR", 150 | "KW", 151 | "KZ", 152 | "LA", 153 | "LB", 154 | "LC", 155 | "LI", 156 | "LK", 157 | "LR", 158 | "LS", 159 | "LT", 160 | "LU", 161 | "LV", 162 | "LY", 163 | "MA", 164 | "MC", 165 | "MD", 166 | "ME", 167 | "MG", 168 | "MH", 169 | "MK", 170 | "ML", 171 | "MN", 172 | "MO", 173 | "MR", 174 | "MT", 175 | "MU", 176 | "MV", 177 | "MW", 178 | "MX", 179 | "MY", 180 | "MZ", 181 | "NA", 182 | "NE", 183 | "NG", 184 | "NI", 185 | "NL", 186 | "NO", 187 | "NP", 188 | "NR", 189 | "NZ", 190 | "OM", 191 | "PA", 192 | "PE", 193 | "PG", 194 | "PH", 195 | "PK", 196 | "PL", 197 | "PS", 198 | "PT", 199 | "PW", 200 | "PY", 201 | "QA", 202 | "RO", 203 | "RS", 204 | "RW", 205 | "SA", 206 | "SB", 207 | "SC", 208 | "SE", 209 | "SG", 210 | "SI", 211 | "SK", 212 | "SL", 213 | "SM", 214 | "SN", 215 | "SR", 216 | "ST", 217 | "SV", 218 | "SZ", 219 | "TD", 220 | "TG", 221 | "TH", 222 | "TJ", 223 | "TL", 224 | "TN", 225 | "TO", 226 | "TR", 227 | "TT", 228 | "TV", 229 | "TW", 230 | "TZ", 231 | "UA", 232 | "UG", 233 | "US", 234 | "UY", 235 | "UZ", 236 | "VC", 237 | "VE", 238 | "VN", 239 | "VU", 240 | "WS", 241 | "XK", 242 | "ZA", 243 | "ZM", 244 | "ZW" 245 | ], 246 | "href": "https://api.spotify.com/v1/albums/79lzvkA5KtvBmKsoPEFJxH", 247 | "images": [ 248 | { 249 | "height": 640, 250 | "width": 640, 251 | "url": "https://i.scdn.co/image/ab67616d0000b273f52ebe232c4f94fd0d0b3b0e" 252 | }, 253 | { 254 | "height": 300, 255 | "width": 300, 256 | "url": "https://i.scdn.co/image/ab67616d00001e02f52ebe232c4f94fd0d0b3b0e" 257 | }, 258 | { 259 | "height": 64, 260 | "width": 64, 261 | "url": "https://i.scdn.co/image/ab67616d00004851f52ebe232c4f94fd0d0b3b0e" 262 | } 263 | ], 264 | "external_urls": { 265 | "spotify": "https://open.spotify.com/album/79lzvkA5KtvBmKsoPEFJxH" 266 | }, 267 | "release_date": "2022-04-21", 268 | "release_date_precision": "day" 269 | }, 270 | "external_ids": { 271 | "isrc": "FRUM72200454" 272 | }, 273 | "popularity": 30, 274 | "is_playable": null, 275 | "linked_from": null, 276 | "artists": [ 277 | { 278 | "name": "Captaine Roshi", 279 | "id": "4bDcCV0zjPsVs2GxtduYry", 280 | "uri": "spotify:artist:4bDcCV0zjPsVs2GxtduYry", 281 | "href": "https://api.spotify.com/v1/artists/4bDcCV0zjPsVs2GxtduYry", 282 | "external_urls": { 283 | "spotify": "https://open.spotify.com/artist/4bDcCV0zjPsVs2GxtduYry" 284 | } 285 | } 286 | ], 287 | "available_markets": [ 288 | "AD", 289 | "AE", 290 | "AG", 291 | "AL", 292 | "AM", 293 | "AO", 294 | "AR", 295 | "AT", 296 | "AU", 297 | "AZ", 298 | "BA", 299 | "BB", 300 | "BD", 301 | "BE", 302 | "BF", 303 | "BG", 304 | "BH", 305 | "BI", 306 | "BJ", 307 | "BN", 308 | "BO", 309 | "BR", 310 | "BS", 311 | "BT", 312 | "BW", 313 | "BZ", 314 | "CA", 315 | "CD", 316 | "CG", 317 | "CH", 318 | "CI", 319 | "CL", 320 | "CM", 321 | "CO", 322 | "CR", 323 | "CV", 324 | "CW", 325 | "CY", 326 | "CZ", 327 | "DE", 328 | "DJ", 329 | "DK", 330 | "DM", 331 | "DO", 332 | "DZ", 333 | "EC", 334 | "EE", 335 | "EG", 336 | "ES", 337 | "ET", 338 | "FI", 339 | "FJ", 340 | "FM", 341 | "FR", 342 | "GA", 343 | "GB", 344 | "GD", 345 | "GE", 346 | "GH", 347 | "GM", 348 | "GN", 349 | "GQ", 350 | "GR", 351 | "GT", 352 | "GW", 353 | "GY", 354 | "HK", 355 | "HN", 356 | "HR", 357 | "HT", 358 | "HU", 359 | "ID", 360 | "IE", 361 | "IL", 362 | "IN", 363 | "IQ", 364 | "IS", 365 | "IT", 366 | "JM", 367 | "JO", 368 | "JP", 369 | "KE", 370 | "KG", 371 | "KH", 372 | "KI", 373 | "KM", 374 | "KN", 375 | "KR", 376 | "KW", 377 | "KZ", 378 | "LA", 379 | "LB", 380 | "LC", 381 | "LI", 382 | "LK", 383 | "LR", 384 | "LS", 385 | "LT", 386 | "LU", 387 | "LV", 388 | "LY", 389 | "MA", 390 | "MC", 391 | "MD", 392 | "ME", 393 | "MG", 394 | "MH", 395 | "MK", 396 | "ML", 397 | "MN", 398 | "MO", 399 | "MR", 400 | "MT", 401 | "MU", 402 | "MV", 403 | "MW", 404 | "MX", 405 | "MY", 406 | "MZ", 407 | "NA", 408 | "NE", 409 | "NG", 410 | "NI", 411 | "NL", 412 | "NO", 413 | "NP", 414 | "NR", 415 | "NZ", 416 | "OM", 417 | "PA", 418 | "PE", 419 | "PG", 420 | "PH", 421 | "PK", 422 | "PL", 423 | "PS", 424 | "PT", 425 | "PW", 426 | "PY", 427 | "QA", 428 | "RO", 429 | "RS", 430 | "RW", 431 | "SA", 432 | "SB", 433 | "SC", 434 | "SE", 435 | "SG", 436 | "SI", 437 | "SK", 438 | "SL", 439 | "SM", 440 | "SN", 441 | "SR", 442 | "ST", 443 | "SV", 444 | "SZ", 445 | "TD", 446 | "TG", 447 | "TH", 448 | "TJ", 449 | "TL", 450 | "TN", 451 | "TO", 452 | "TR", 453 | "TT", 454 | "TV", 455 | "TW", 456 | "TZ", 457 | "UA", 458 | "UG", 459 | "US", 460 | "UY", 461 | "UZ", 462 | "VC", 463 | "VE", 464 | "VN", 465 | "VU", 466 | "WS", 467 | "XK", 468 | "ZA", 469 | "ZM", 470 | "ZW" 471 | ], 472 | "disc_number": 1, 473 | "duration_ms": 216400, 474 | "explicit": true, 475 | "external_urls": { 476 | "spotify": "https://open.spotify.com/track/7cL8WJQ4blKK6gtp7Ug0WT" 477 | }, 478 | "href": "https://api.spotify.com/v1/tracks/7cL8WJQ4blKK6gtp7Ug0WT", 479 | "id": "7cL8WJQ4blKK6gtp7Ug0WT", 480 | "name": "Croc aiguisé", 481 | "preview_url": "https://p.scdn.co/mp3-preview/e8ffd7089256403313df10b44926146d36d67b62?cid=e44e7b8278114c7db211c00ea273ac69", 482 | "track_number": 16, 483 | "uri": "spotify:track:7cL8WJQ4blKK6gtp7Ug0WT" 484 | } 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 94 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 95 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 96 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 97 | ); 98 | path = Runner; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | 97C146ED1CF9000F007C117D /* Runner */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 107 | buildPhases = ( 108 | 9740EEB61CF901F6004384FC /* Run Script */, 109 | 97C146EA1CF9000F007C117D /* Sources */, 110 | 97C146EB1CF9000F007C117D /* Frameworks */, 111 | 97C146EC1CF9000F007C117D /* Resources */, 112 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 113 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = Runner; 120 | productName = Runner; 121 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | /* End PBXNativeTarget section */ 125 | 126 | /* Begin PBXProject section */ 127 | 97C146E61CF9000F007C117D /* Project object */ = { 128 | isa = PBXProject; 129 | attributes = { 130 | LastUpgradeCheck = 1300; 131 | ORGANIZATIONNAME = ""; 132 | TargetAttributes = { 133 | 97C146ED1CF9000F007C117D = { 134 | CreatedOnToolsVersion = 7.3.1; 135 | LastSwiftMigration = 1100; 136 | }; 137 | }; 138 | }; 139 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 140 | compatibilityVersion = "Xcode 9.3"; 141 | developmentRegion = en; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | Base, 146 | ); 147 | mainGroup = 97C146E51CF9000F007C117D; 148 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 149 | projectDirPath = ""; 150 | projectRoot = ""; 151 | targets = ( 152 | 97C146ED1CF9000F007C117D /* Runner */, 153 | ); 154 | }; 155 | /* End PBXProject section */ 156 | 157 | /* Begin PBXResourcesBuildPhase section */ 158 | 97C146EC1CF9000F007C117D /* Resources */ = { 159 | isa = PBXResourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 163 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 164 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 165 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXShellScriptBuildPhase section */ 172 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 173 | isa = PBXShellScriptBuildPhase; 174 | alwaysOutOfDate = 1; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | ); 178 | inputPaths = ( 179 | ); 180 | name = "Thin Binary"; 181 | outputPaths = ( 182 | ); 183 | runOnlyForDeploymentPostprocessing = 0; 184 | shellPath = /bin/sh; 185 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 186 | }; 187 | 9740EEB61CF901F6004384FC /* Run Script */ = { 188 | isa = PBXShellScriptBuildPhase; 189 | alwaysOutOfDate = 1; 190 | buildActionMask = 2147483647; 191 | files = ( 192 | ); 193 | inputPaths = ( 194 | ); 195 | name = "Run Script"; 196 | outputPaths = ( 197 | ); 198 | runOnlyForDeploymentPostprocessing = 0; 199 | shellPath = /bin/sh; 200 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 201 | }; 202 | /* End PBXShellScriptBuildPhase section */ 203 | 204 | /* Begin PBXSourcesBuildPhase section */ 205 | 97C146EA1CF9000F007C117D /* Sources */ = { 206 | isa = PBXSourcesBuildPhase; 207 | buildActionMask = 2147483647; 208 | files = ( 209 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 210 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 211 | ); 212 | runOnlyForDeploymentPostprocessing = 0; 213 | }; 214 | /* End PBXSourcesBuildPhase section */ 215 | 216 | /* Begin PBXVariantGroup section */ 217 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 218 | isa = PBXVariantGroup; 219 | children = ( 220 | 97C146FB1CF9000F007C117D /* Base */, 221 | ); 222 | name = Main.storyboard; 223 | sourceTree = ""; 224 | }; 225 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 226 | isa = PBXVariantGroup; 227 | children = ( 228 | 97C147001CF9000F007C117D /* Base */, 229 | ); 230 | name = LaunchScreen.storyboard; 231 | sourceTree = ""; 232 | }; 233 | /* End PBXVariantGroup section */ 234 | 235 | /* Begin XCBuildConfiguration section */ 236 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 237 | isa = XCBuildConfiguration; 238 | buildSettings = { 239 | ALWAYS_SEARCH_USER_PATHS = NO; 240 | CLANG_ANALYZER_NONNULL = YES; 241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 242 | CLANG_CXX_LIBRARY = "libc++"; 243 | CLANG_ENABLE_MODULES = YES; 244 | CLANG_ENABLE_OBJC_ARC = YES; 245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 246 | CLANG_WARN_BOOL_CONVERSION = YES; 247 | CLANG_WARN_COMMA = YES; 248 | CLANG_WARN_CONSTANT_CONVERSION = YES; 249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 250 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 251 | CLANG_WARN_EMPTY_BODY = YES; 252 | CLANG_WARN_ENUM_CONVERSION = YES; 253 | CLANG_WARN_INFINITE_RECURSION = YES; 254 | CLANG_WARN_INT_CONVERSION = YES; 255 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 257 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 258 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 259 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 260 | CLANG_WARN_STRICT_PROTOTYPES = YES; 261 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 262 | CLANG_WARN_UNREACHABLE_CODE = YES; 263 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 264 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 265 | COPY_PHASE_STRIP = NO; 266 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 267 | ENABLE_NS_ASSERTIONS = NO; 268 | ENABLE_STRICT_OBJC_MSGSEND = YES; 269 | GCC_C_LANGUAGE_STANDARD = gnu99; 270 | GCC_NO_COMMON_BLOCKS = YES; 271 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 272 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 273 | GCC_WARN_UNDECLARED_SELECTOR = YES; 274 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 275 | GCC_WARN_UNUSED_FUNCTION = YES; 276 | GCC_WARN_UNUSED_VARIABLE = YES; 277 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 278 | MTL_ENABLE_DEBUG_INFO = NO; 279 | SDKROOT = iphoneos; 280 | SUPPORTED_PLATFORMS = iphoneos; 281 | TARGETED_DEVICE_FAMILY = "1,2"; 282 | VALIDATE_PRODUCT = YES; 283 | }; 284 | name = Profile; 285 | }; 286 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 287 | isa = XCBuildConfiguration; 288 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 289 | buildSettings = { 290 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 291 | CLANG_ENABLE_MODULES = YES; 292 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 293 | ENABLE_BITCODE = NO; 294 | INFOPLIST_FILE = Runner/Info.plist; 295 | LD_RUNPATH_SEARCH_PATHS = ( 296 | "$(inherited)", 297 | "@executable_path/Frameworks", 298 | ); 299 | PRODUCT_BUNDLE_IDENTIFIER = com.stevenosse.sequence; 300 | PRODUCT_NAME = "$(TARGET_NAME)"; 301 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 302 | SWIFT_VERSION = 5.0; 303 | VERSIONING_SYSTEM = "apple-generic"; 304 | }; 305 | name = Profile; 306 | }; 307 | 97C147031CF9000F007C117D /* Debug */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ALWAYS_SEARCH_USER_PATHS = NO; 311 | CLANG_ANALYZER_NONNULL = YES; 312 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 313 | CLANG_CXX_LIBRARY = "libc++"; 314 | CLANG_ENABLE_MODULES = YES; 315 | CLANG_ENABLE_OBJC_ARC = YES; 316 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 317 | CLANG_WARN_BOOL_CONVERSION = YES; 318 | CLANG_WARN_COMMA = YES; 319 | CLANG_WARN_CONSTANT_CONVERSION = YES; 320 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 321 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 322 | CLANG_WARN_EMPTY_BODY = YES; 323 | CLANG_WARN_ENUM_CONVERSION = YES; 324 | CLANG_WARN_INFINITE_RECURSION = YES; 325 | CLANG_WARN_INT_CONVERSION = YES; 326 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 328 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 329 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 330 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 331 | CLANG_WARN_STRICT_PROTOTYPES = YES; 332 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 333 | CLANG_WARN_UNREACHABLE_CODE = YES; 334 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 335 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 336 | COPY_PHASE_STRIP = NO; 337 | DEBUG_INFORMATION_FORMAT = dwarf; 338 | ENABLE_STRICT_OBJC_MSGSEND = YES; 339 | ENABLE_TESTABILITY = YES; 340 | GCC_C_LANGUAGE_STANDARD = gnu99; 341 | GCC_DYNAMIC_NO_PIC = NO; 342 | GCC_NO_COMMON_BLOCKS = YES; 343 | GCC_OPTIMIZATION_LEVEL = 0; 344 | GCC_PREPROCESSOR_DEFINITIONS = ( 345 | "DEBUG=1", 346 | "$(inherited)", 347 | ); 348 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 349 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 350 | GCC_WARN_UNDECLARED_SELECTOR = YES; 351 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 352 | GCC_WARN_UNUSED_FUNCTION = YES; 353 | GCC_WARN_UNUSED_VARIABLE = YES; 354 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 355 | MTL_ENABLE_DEBUG_INFO = YES; 356 | ONLY_ACTIVE_ARCH = YES; 357 | SDKROOT = iphoneos; 358 | TARGETED_DEVICE_FAMILY = "1,2"; 359 | }; 360 | name = Debug; 361 | }; 362 | 97C147041CF9000F007C117D /* Release */ = { 363 | isa = XCBuildConfiguration; 364 | buildSettings = { 365 | ALWAYS_SEARCH_USER_PATHS = NO; 366 | CLANG_ANALYZER_NONNULL = YES; 367 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 368 | CLANG_CXX_LIBRARY = "libc++"; 369 | CLANG_ENABLE_MODULES = YES; 370 | CLANG_ENABLE_OBJC_ARC = YES; 371 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 372 | CLANG_WARN_BOOL_CONVERSION = YES; 373 | CLANG_WARN_COMMA = YES; 374 | CLANG_WARN_CONSTANT_CONVERSION = YES; 375 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 376 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 377 | CLANG_WARN_EMPTY_BODY = YES; 378 | CLANG_WARN_ENUM_CONVERSION = YES; 379 | CLANG_WARN_INFINITE_RECURSION = YES; 380 | CLANG_WARN_INT_CONVERSION = YES; 381 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 382 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 383 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 384 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 385 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 386 | CLANG_WARN_STRICT_PROTOTYPES = YES; 387 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 388 | CLANG_WARN_UNREACHABLE_CODE = YES; 389 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 390 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 391 | COPY_PHASE_STRIP = NO; 392 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 393 | ENABLE_NS_ASSERTIONS = NO; 394 | ENABLE_STRICT_OBJC_MSGSEND = YES; 395 | GCC_C_LANGUAGE_STANDARD = gnu99; 396 | GCC_NO_COMMON_BLOCKS = YES; 397 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 398 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 399 | GCC_WARN_UNDECLARED_SELECTOR = YES; 400 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 401 | GCC_WARN_UNUSED_FUNCTION = YES; 402 | GCC_WARN_UNUSED_VARIABLE = YES; 403 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 404 | MTL_ENABLE_DEBUG_INFO = NO; 405 | SDKROOT = iphoneos; 406 | SUPPORTED_PLATFORMS = iphoneos; 407 | SWIFT_COMPILATION_MODE = wholemodule; 408 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 409 | TARGETED_DEVICE_FAMILY = "1,2"; 410 | VALIDATE_PRODUCT = YES; 411 | }; 412 | name = Release; 413 | }; 414 | 97C147061CF9000F007C117D /* Debug */ = { 415 | isa = XCBuildConfiguration; 416 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 417 | buildSettings = { 418 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 419 | CLANG_ENABLE_MODULES = YES; 420 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 421 | ENABLE_BITCODE = NO; 422 | INFOPLIST_FILE = Runner/Info.plist; 423 | LD_RUNPATH_SEARCH_PATHS = ( 424 | "$(inherited)", 425 | "@executable_path/Frameworks", 426 | ); 427 | PRODUCT_BUNDLE_IDENTIFIER = com.stevenosse.sequence; 428 | PRODUCT_NAME = "$(TARGET_NAME)"; 429 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 430 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 431 | SWIFT_VERSION = 5.0; 432 | VERSIONING_SYSTEM = "apple-generic"; 433 | }; 434 | name = Debug; 435 | }; 436 | 97C147071CF9000F007C117D /* Release */ = { 437 | isa = XCBuildConfiguration; 438 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 439 | buildSettings = { 440 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 441 | CLANG_ENABLE_MODULES = YES; 442 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 443 | ENABLE_BITCODE = NO; 444 | INFOPLIST_FILE = Runner/Info.plist; 445 | LD_RUNPATH_SEARCH_PATHS = ( 446 | "$(inherited)", 447 | "@executable_path/Frameworks", 448 | ); 449 | PRODUCT_BUNDLE_IDENTIFIER = com.stevenosse.sequence; 450 | PRODUCT_NAME = "$(TARGET_NAME)"; 451 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 452 | SWIFT_VERSION = 5.0; 453 | VERSIONING_SYSTEM = "apple-generic"; 454 | }; 455 | name = Release; 456 | }; 457 | /* End XCBuildConfiguration section */ 458 | 459 | /* Begin XCConfigurationList section */ 460 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 461 | isa = XCConfigurationList; 462 | buildConfigurations = ( 463 | 97C147031CF9000F007C117D /* Debug */, 464 | 97C147041CF9000F007C117D /* Release */, 465 | 249021D3217E4FDB00AE95B9 /* Profile */, 466 | ); 467 | defaultConfigurationIsVisible = 0; 468 | defaultConfigurationName = Release; 469 | }; 470 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 471 | isa = XCConfigurationList; 472 | buildConfigurations = ( 473 | 97C147061CF9000F007C117D /* Debug */, 474 | 97C147071CF9000F007C117D /* Release */, 475 | 249021D4217E4FDB00AE95B9 /* Profile */, 476 | ); 477 | defaultConfigurationIsVisible = 0; 478 | defaultConfigurationName = Release; 479 | }; 480 | /* End XCConfigurationList section */ 481 | }; 482 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 483 | } 484 | --------------------------------------------------------------------------------