├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 16.png │ │ │ ├── 20.png │ │ │ ├── 29.png │ │ │ ├── 32.png │ │ │ ├── 40.png │ │ │ ├── 48.png │ │ │ ├── 50.png │ │ │ ├── 55.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 64.png │ │ │ ├── 72.png │ │ │ ├── 76.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ ├── 88.png │ │ │ ├── 100.png │ │ │ ├── 1024.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 128.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 167.png │ │ │ ├── 172.png │ │ │ ├── 180.png │ │ │ ├── 196.png │ │ │ ├── 216.png │ │ │ ├── 256.png │ │ │ └── 512.png │ │ └── LaunchImage.imageset │ │ │ ├── ic_splash.png │ │ │ ├── ic_splash 1.png │ │ │ ├── ic_splash 2.png │ │ │ ├── README.md │ │ │ └── Contents.json │ ├── Runner.entitlements │ ├── AppDelegate.swift │ ├── GoogleService-Info.plist │ ├── Info.plist │ └── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── firebase_app_id_file.json ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── .gitignore └── Podfile ├── assets └── logo.png ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── .well-known │ ├── apple-app-site-association │ └── assetlinks.json ├── manifest.json └── index.html ├── .github ├── screenshot_1.png ├── screenshot_2.png └── screenshot_3.png ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── ic_launcher-web.png │ │ │ │ ├── playstore-icon.png │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_splash.png │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_action_play.png │ │ │ │ │ ├── ic_action_heart.png │ │ │ │ │ ├── ic_action_pause.png │ │ │ │ │ ├── ic_action_skip_back.png │ │ │ │ │ ├── ic_action_unheart.png │ │ │ │ │ ├── ic_action_skip_forward.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_splash.png │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_action_play.png │ │ │ │ │ ├── ic_action_heart.png │ │ │ │ │ ├── ic_action_pause.png │ │ │ │ │ ├── ic_action_skip_back.png │ │ │ │ │ ├── ic_action_unheart.png │ │ │ │ │ ├── ic_action_skip_forward.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_splash.png │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_splash.png │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_splash.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── raw │ │ │ │ │ └── keep.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ ├── drawable │ │ │ │ │ ├── launch_background.xml │ │ │ │ │ ├── launch_gradient_background.xml │ │ │ │ │ └── animated_launch_icon.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ ├── launch_background.xml │ │ │ │ │ ├── launch_gradient_background.xml │ │ │ │ │ └── animated_launch_icon.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── one │ │ │ │ │ └── tear │ │ │ │ │ └── tearmusic │ │ │ │ │ └── MainActivity.kt │ │ │ ├── java │ │ │ │ └── io │ │ │ │ │ └── flutter │ │ │ │ │ └── app │ │ │ │ │ └── FlutterMultiDexApplication.java │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── google-services.json │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── changelog.md ├── fonts └── Montserrat │ ├── Montserrat-Black.otf │ ├── Montserrat-Bold.otf │ ├── Montserrat-Light.otf │ ├── Montserrat-Thin.otf │ ├── Montserrat-Italic.otf │ ├── Montserrat-Medium.otf │ ├── Montserrat-Regular.otf │ ├── Montserrat-SemiBold.otf │ ├── Montserrat-BoldItalic.otf │ ├── Montserrat-ExtraBold.otf │ ├── Montserrat-ExtraLight.otf │ ├── Montserrat-ThinItalic.otf │ ├── Montserrat-BlackItalic.otf │ ├── Montserrat-LightItalic.otf │ ├── Montserrat-MediumItalic.otf │ ├── Montserrat-ExtraBoldItalic.otf │ ├── Montserrat-SemiBoldItalic.otf │ └── Montserrat-ExtraLightItalic.otf ├── lib ├── providers │ ├── will_pop_provider.dart │ ├── audio_stream_provider.dart │ ├── current_music_provider.dart │ ├── theme_provider.dart │ └── navigator_provider.dart ├── ui │ ├── common │ │ ├── format.dart │ │ └── image_color.dart │ └── mobile │ │ ├── common │ │ ├── tm_back_button.dart │ │ ├── tiles │ │ │ ├── search_track_tile.dart │ │ │ ├── artist_track_tile.dart │ │ │ ├── album_track_tile.dart │ │ │ ├── track_tile_preview.dart │ │ │ ├── search_album_tile.dart │ │ │ ├── search_playlist_tile.dart │ │ │ ├── search_artist_tile.dart │ │ │ ├── manual_match_tile.dart │ │ │ ├── artist_artist_tile.dart │ │ │ └── artist_album_tile.dart │ │ ├── views │ │ │ ├── playlist_track_tile.dart │ │ │ ├── artist_view │ │ │ │ ├── artist_header_button.dart │ │ │ │ └── latest_release.dart │ │ │ ├── content_list_view.dart │ │ │ └── manual_match_view.dart │ │ ├── player │ │ │ ├── lyrics_view │ │ │ │ ├── full_text.dart │ │ │ │ ├── unavailable.dart │ │ │ │ ├── subtitle.dart │ │ │ │ └── rich_sync.dart │ │ │ ├── image_placeholder.dart │ │ │ └── track_image.dart │ │ ├── settings │ │ │ ├── settings_stats.dart │ │ │ ├── settings_container.dart │ │ │ ├── settings_switch.dart │ │ │ └── settings_alert_dialog.dart │ │ ├── profile_button.dart │ │ ├── knob.dart │ │ ├── view_menu_button.dart │ │ ├── menu_button.dart │ │ ├── bottom_sheet.dart │ │ ├── filter_bar.dart │ │ └── wallpaper.dart │ │ ├── app.dart │ │ └── pages │ │ ├── library │ │ ├── artist_loading_tile.dart │ │ ├── album_loading_tile.dart │ │ ├── track_loading_tile.dart │ │ └── playlist_loading_tile.dart │ │ └── search │ │ └── top_result_container.dart ├── exceptionts.dart ├── models │ ├── user_info.dart │ ├── manual_match.dart │ ├── model.dart │ ├── segmented.dart │ ├── playback.dart │ ├── music │ │ ├── images.dart │ │ ├── artist.dart │ │ ├── track.dart │ │ ├── playlist.dart │ │ ├── lyrics.dart │ │ └── album.dart │ ├── library.dart │ ├── batch.dart │ ├── search.dart │ └── player_info.dart ├── utils.dart ├── player │ └── media_control.dart ├── api │ ├── base_api.dart │ └── user_api.dart ├── firebase_options.dart └── main.dart ├── .vscode └── launch.json ├── README.md ├── .gitignore ├── .metadata ├── analysis_options.yaml └── pubspec.yaml /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/assets/logo.png -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/web/icons/Icon-512.png -------------------------------------------------------------------------------- /.github/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/.github/screenshot_1.png -------------------------------------------------------------------------------- /.github/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/.github/screenshot_2.png -------------------------------------------------------------------------------- /.github/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/.github/screenshot_3.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | - new icons 2 | - improved animations 3 | - scrollbars 4 | - modal bottom sheets for views 5 | - bugfixes 6 | -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-Black.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-Bold.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-Light.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-Thin.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-Thin.otf -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-Italic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-Italic.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-Medium.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-Regular.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-SemiBold.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-BoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-BoldItalic.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-ExtraBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-ExtraBold.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-ExtraLight.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-ExtraLight.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-ThinItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-ThinItalic.otf -------------------------------------------------------------------------------- /android/app/src/main/res/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/ic_launcher-web.png -------------------------------------------------------------------------------- /android/app/src/main/res/playstore-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/playstore-icon.png -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-BlackItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-BlackItalic.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-LightItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-LightItalic.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-MediumItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-MediumItalic.otf -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-ExtraBoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-ExtraBoldItalic.otf -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-SemiBoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-SemiBoldItalic.otf -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-hdpi/ic_splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-mdpi/ic_splash.png -------------------------------------------------------------------------------- /fonts/Montserrat/Montserrat-ExtraLightItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/fonts/Montserrat/Montserrat-ExtraLightItalic.otf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/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/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_splash.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_action_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-hdpi/ic_action_play.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_action_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-mdpi/ic_action_play.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/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/tearone/tearmusic/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/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_splash.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_action_heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-hdpi/ic_action_heart.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_action_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-hdpi/ic_action_pause.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_action_heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-mdpi/ic_action_heart.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_action_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-mdpi/ic_action_pause.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_action_skip_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-hdpi/ic_action_skip_back.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_action_unheart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-hdpi/ic_action_unheart.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_action_skip_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-mdpi/ic_action_skip_back.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_action_unheart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-mdpi/ic_action_unheart.png -------------------------------------------------------------------------------- /android/app/src/main/res/raw/keep.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/ic_splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_action_skip_forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-hdpi/ic_action_skip_forward.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_action_skip_forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-mdpi/ic_action_skip_forward.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/ic_splash 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/ic_splash 1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/ic_splash 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/ic_splash 2.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tearone/tearmusic/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/one/tear/tearmusic/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package one.tear.tearmusic 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 | -------------------------------------------------------------------------------- /lib/providers/will_pop_provider.dart: -------------------------------------------------------------------------------- 1 | class WillPopProvider { 2 | bool Function()? _popper; 3 | 4 | bool Function()? get popper => _popper; 5 | 6 | void registerPopper(bool Function() value) { 7 | _popper = value; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /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.4-all.zip 6 | -------------------------------------------------------------------------------- /web/.well-known/apple-app-site-association: -------------------------------------------------------------------------------- 1 | { 2 | "applinks": { 3 | "apps": [], 4 | "details": [ 5 | { 6 | "appID": "MYUTW2GF6J.one.tear.tearmusic", 7 | "paths": [ 8 | "*" 9 | ] 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/firebase_app_id_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_generated_by": "FlutterFire CLI", 3 | "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", 4 | "GOOGLE_APP_ID": "1:860150371329:ios:9430cbf0de56c4ec651c00", 5 | "FIREBASE_PROJECT_ID": "tear-music", 6 | "GCM_SENDER_ID": "860150371329" 7 | } -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/.well-known/assetlinks.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "relation": ["delegate_permission/common.handle_all_urls"], 3 | "target": { 4 | "namespace": "android_app", 5 | "package_name": "one.tear.tearmusic", 6 | "sha256_cert_fingerprints": 7 | ["98:9F:DA:C2:3D:0A:A8:A3:C0:92:22:D5:20:1E:B5:47:80:76:53:A8:62:9C:E3:C7:08:32:E6:42:82:1D:7A:69"] 8 | } 9 | }] 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_gradient_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_gradient_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.associated-domains 6 | 7 | applinks:music.tear.one 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/ui/common/format.dart: -------------------------------------------------------------------------------- 1 | extension DurationFormat on Duration { 2 | String format() { 3 | return "${inHours > 0 ? '$inHours h ' : ''}${inMinutes % 60} mins"; 4 | } 5 | 6 | String shortFormat() { 7 | return "${inHours > 0 ? '$inHours:' : ''}${(inMinutes % 60).toString().padLeft(inHours > 0 ? 2 : 0, '0')}:${(inSeconds % 60).toString().padLeft(2, '0')}"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /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/ui/mobile/common/tm_back_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:tearmusic/providers/navigator_provider.dart'; 4 | 5 | class TMBackButton extends StatelessWidget { 6 | const TMBackButton({Key? key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return BackButton( 11 | onPressed: () => context.read().pop(), 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/tiles/search_track_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tearmusic/models/music/track.dart'; 3 | import 'package:tearmusic/ui/mobile/common/tiles/track_tile.dart'; 4 | 5 | class SearchTrackTile extends StatelessWidget { 6 | const SearchTrackTile(this.track, {Key? key}) : super(key: key); 7 | 8 | final MusicTrack track; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return TrackTile(track); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/exceptionts.dart: -------------------------------------------------------------------------------- 1 | class BaseRequestException { 2 | String cause; 3 | 4 | BaseRequestException(this.cause); 5 | } 6 | 7 | class AuthException extends BaseRequestException { 8 | AuthException(String cause) : super(cause); 9 | } 10 | 11 | class NotFoundException extends BaseRequestException { 12 | NotFoundException(String cause) : super(cause); 13 | } 14 | 15 | class UnknownRequestException extends BaseRequestException { 16 | UnknownRequestException(String cause) : super(cause); 17 | } 18 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/tiles/artist_track_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tearmusic/models/music/track.dart'; 3 | import 'package:tearmusic/ui/mobile/common/tiles/track_tile.dart'; 4 | 5 | class ArtistTrackTile extends StatelessWidget { 6 | const ArtistTrackTile(this.track, {Key? key}) : super(key: key); 7 | 8 | final MusicTrack track; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return TrackTile(track, trailingDuration: true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/views/playlist_track_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tearmusic/models/music/track.dart'; 3 | import 'package:tearmusic/ui/mobile/common/tiles/track_tile.dart'; 4 | 5 | class PlaylistTrackTile extends StatelessWidget { 6 | const PlaylistTrackTile(this.track, {Key? key}) : super(key: key); 7 | 8 | final MusicTrack track; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return TrackTile(track, trailingDuration: true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/providers/audio_stream_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | import 'dart:io'; 3 | 4 | class AudioStreamProvider { 5 | late final ServerSocket socket; 6 | late final HttpServer server; 7 | 8 | Future startServer() async { 9 | socket = await ServerSocket.bind("127.0.0.1", 0); 10 | server = HttpServer.listenOn(socket); 11 | log("Started proxy server on $socket"); 12 | } 13 | 14 | Future stopServer({bool force = false}) async { 15 | await server.close(force: force); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/tiles/album_track_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tearmusic/models/music/track.dart'; 3 | import 'package:tearmusic/ui/mobile/common/tiles/track_tile.dart'; 4 | 5 | class AlbumTrackTile extends StatelessWidget { 6 | const AlbumTrackTile(this.track, {Key? key}) : super(key: key); 7 | 8 | final MusicTrack track; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return TrackTile(track, leadingTrackNumber: true, trailingDuration: true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ic_splash.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ic_splash 1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "ic_splash 2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/models/user_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:tearmusic/models/model.dart'; 2 | 3 | class UserInfo extends Model { 4 | String username; 5 | String avatar; 6 | 7 | UserInfo({ 8 | required String id, 9 | required Map json, 10 | required this.username, 11 | required this.avatar, 12 | }) : super(id: id, json: json, key: username, type: "user"); 13 | 14 | factory UserInfo.decode(Map json) { 15 | return UserInfo( 16 | json: json, 17 | id: json["discord_id"], 18 | username: json["username"], 19 | avatar: json["avatar"], 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/player/lyrics_view/full_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LyricsFullText extends StatelessWidget { 4 | const LyricsFullText(this.fullText, {Key? key}) : super(key: key); 5 | 6 | final String fullText; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Padding( 11 | padding: const EdgeInsets.all(8.0), 12 | child: Text( 13 | fullText, 14 | style: const TextStyle( 15 | fontSize: 24.0, 16 | fontWeight: FontWeight.bold, 17 | ), 18 | ), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/utils.dart: -------------------------------------------------------------------------------- 1 | double rangeProgress({ 2 | required final double a, 3 | required final double b, 4 | required final double c, 5 | }) { 6 | return c * (b - a) + a; 7 | } 8 | 9 | double progressValue({ 10 | required final double min, 11 | required final double max, 12 | required final double value, 13 | }) { 14 | return (value - min) / (max - min); 15 | } 16 | 17 | double norm(double val, double minVal, double maxVal, double newMin, double newMax) { 18 | return newMin + (val - minVal) * (newMax - newMin) / (maxVal - minVal); 19 | } 20 | 21 | double inverseAboveOne(double n) { 22 | if (n > 1) return (1 - (1 - n) * -1); 23 | return n; 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "tearmusic", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "tearmusic (profile mode)", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | }, 18 | { 19 | "name": "tearmusic (release mode)", 20 | "request": "launch", 21 | "type": "dart", 22 | "flutterMode": "release" 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 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/player/image_placeholder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ImagePlaceholder extends StatelessWidget { 4 | const ImagePlaceholder({Key? key, this.large = false}) : super(key: key); 5 | 6 | final bool large; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return SizedBox( 11 | width: 150.0, 12 | height: 150.0, 13 | child: ClipRRect( 14 | borderRadius: BorderRadius.circular(12.0), 15 | child: Image.network( 16 | "https://random.imagecdn.app/${large ? 400 : 200}/${large ? 400 : 200}?$key", 17 | width: 150.0, 18 | height: 150.0, 19 | fit: BoxFit.cover, 20 | ), 21 | ), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/models/manual_match.dart: -------------------------------------------------------------------------------- 1 | class ManualMatch { 2 | final String name; 3 | final String artist; 4 | final String imageUrl; 5 | final Duration duration; 6 | final String videoId; 7 | 8 | ManualMatch({ 9 | required this.name, 10 | required this.artist, 11 | required this.imageUrl, 12 | required this.duration, 13 | required this.videoId, 14 | }); 15 | 16 | factory ManualMatch.decode(Map json) { 17 | return ManualMatch( 18 | name: json['name'], 19 | artist: json['artist'], 20 | imageUrl: json['image'], 21 | duration: Duration(seconds: json['duration'].toInt()), 22 | videoId: json['video_id'], 23 | ); 24 | } 25 | 26 | static List decodeList(List list) => list.map((e) => ManualMatch.decode(e)).toList().cast(); 27 | } 28 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java: -------------------------------------------------------------------------------- 1 | // Generated file. 2 | // 3 | // If you wish to remove Flutter's multidex support, delete this entire file. 4 | // 5 | // Modifications to this file should be done in a copy under a different name 6 | // as this file may be regenerated. 7 | 8 | package io.flutter.app; 9 | 10 | import android.app.Application; 11 | import android.content.Context; 12 | import androidx.annotation.CallSuper; 13 | import androidx.multidex.MultiDex; 14 | 15 | /** 16 | * Extension of {@link android.app.Application}, adding multidex support. 17 | */ 18 | public class FlutterMultiDexApplication extends Application { 19 | @Override 20 | @CallSuper 21 | protected void attachBaseContext(Context base) { 22 | super.attachBaseContext(base); 23 | MultiDex.install(this); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/tiles/track_tile_preview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tearmusic/models/music/track.dart'; 3 | import 'package:tearmusic/ui/mobile/common/tiles/track_tile.dart'; 4 | 5 | class TrackTilePreview extends StatelessWidget { 6 | const TrackTilePreview(this.track, {Key? key}) : super(key: key); 7 | 8 | final MusicTrack track; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return ClipRRect( 13 | borderRadius: BorderRadius.circular(12.0), 14 | child: Container( 15 | color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(.5), 16 | child: TrackTile( 17 | track, 18 | onLongPressed: () {}, 19 | onPressed: () {}, 20 | trailingDuration: true, 21 | ), 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tear Music 2 | 3 | [![Codemagic build status](https://api.codemagic.io/apps/62ed9594afdb912d0c4199ff/62ed9594afdb912d0c4199fe/status_badge.svg)](https://codemagic.io/apps/62ed9594afdb912d0c4199ff/62ed9594afdb912d0c4199fe/latest_build) 4 | 5 | > **Tear Music** is a music player front-end built with **Flutter** for custom music streaming back-ends. 6 | 7 | ## Disclaimer 8 | 9 | > Tear Music was not developed for pirating music but educational and private usage. 10 | > It may be illegal to use this in your country, I am not responsible in any way for the usage of others. 11 | 12 | ## Screenshots 13 | 14 | | ![screenshot](.github/screenshot_3.png) | ![screenshot](.github/screenshot_2.png) | ![screenshot](.github/screenshot_1.png) | 15 | | ----------------------------------------- | ----------------------------------------- | ----------------------------------------- | 16 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | // START: FlutterFire Configuration 11 | classpath 'com.google.gms:google-services:4.3.10' 12 | // END: FlutterFire Configuration 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | rootProject.buildDir = '../build' 25 | subprojects { 26 | project.buildDir = "${rootProject.buildDir}/${project.name}" 27 | } 28 | subprojects { 29 | project.evaluationDependsOn(':app') 30 | } 31 | 32 | task clean(type: Delete) { 33 | delete rootProject.buildDir 34 | } 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | # fvm 47 | .fvm 48 | .vscode/settings.json -------------------------------------------------------------------------------- /lib/ui/mobile/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:provider/single_child_widget.dart'; 4 | import 'package:tearmusic/providers/theme_provider.dart'; 5 | import 'package:tearmusic/ui/mobile/navigator.dart'; 6 | 7 | class App extends StatelessWidget { 8 | const App({super.key, required this.providers}); 9 | 10 | final List providers; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return MultiProvider( 15 | providers: providers, 16 | child: Builder(builder: (context) { 17 | return MaterialApp( 18 | title: 'Tear Music', 19 | debugShowCheckedModeBanner: false, 20 | themeMode: ThemeMode.dark, 21 | darkTheme: context.select((e) => e.appTheme), 22 | home: const NavigationScreen(), 23 | ); 24 | }), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.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: 7ac27ac8e6a42750c475ba8a2a3c7047b93fd949 8 | channel: beta 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 7ac27ac8e6a42750c475ba8a2a3c7047b93fd949 17 | base_revision: 7ac27ac8e6a42750c475ba8a2a3c7047b93fd949 18 | - platform: web 19 | create_revision: 7ac27ac8e6a42750c475ba8a2a3c7047b93fd949 20 | base_revision: 7ac27ac8e6a42750c475ba8a2a3c7047b93fd949 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/settings/settings_stats.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SettingsStatsItem extends StatelessWidget { 4 | const SettingsStatsItem({ 5 | Key? key, 6 | required this.name, 7 | required this.value, 8 | }) : super(key: key); 9 | 10 | final String value; 11 | final String name; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Expanded( 16 | child: Card( 17 | elevation: 0.0, 18 | child: Container( 19 | padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 8.0), 20 | child: Column( 21 | children: [ 22 | Text( 23 | value, 24 | style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18.0), 25 | ), 26 | Text( 27 | name, 28 | style: const TextStyle(fontWeight: FontWeight.w300, fontSize: 11.0), 29 | ), 30 | ], 31 | ), 32 | ), 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tearmusic", 3 | "short_name": "tearmusic", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /lib/player/media_control.dart: -------------------------------------------------------------------------------- 1 | import 'package:audio_service/audio_service.dart'; 2 | 3 | class TMMediaControl { 4 | static const play = MediaControl( 5 | action: MediaAction.play, 6 | label: "Play", 7 | androidIcon: "mipmap/ic_action_play", 8 | ); 9 | static const pause = MediaControl( 10 | action: MediaAction.pause, 11 | label: "Pause", 12 | androidIcon: "mipmap/ic_action_pause", 13 | ); 14 | 15 | static const skipForward = MediaControl( 16 | action: MediaAction.skipToNext, 17 | label: "Skip Forward", 18 | androidIcon: "mipmap/ic_action_skip_forward", 19 | ); 20 | static const skipBack = MediaControl( 21 | action: MediaAction.skipToPrevious, 22 | label: "Skip Back", 23 | androidIcon: "mipmap/ic_action_skip_back", 24 | ); 25 | 26 | static const heart = MediaControl( 27 | action: MediaAction.setRating, 28 | label: "Like", 29 | androidIcon: "mipmap/ic_action_heart", 30 | ); 31 | static const unheart = MediaControl( 32 | action: MediaAction.setRating, 33 | label: "Unlike", 34 | androidIcon: "mipmap/ic_action_unheart", 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "860150371329", 4 | "project_id": "tear-music", 5 | "storage_bucket": "tear-music.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:860150371329:android:00ba04b1deea5770651c00", 11 | "android_client_info": { 12 | "package_name": "one.tear.tearmusic" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "860150371329-p4uhg6v89tc9482mhnr6l1622vv2up9b.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyABQJo8Wt9mtfqSh1a2k1LuB0y1w7FhxCw" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "860150371329-p4uhg6v89tc9482mhnr6l1622vv2up9b.apps.googleusercontent.com", 31 | "client_type": 3 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | ], 38 | "configuration_version": "1" 39 | } -------------------------------------------------------------------------------- /lib/ui/mobile/common/tiles/search_album_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:tearmusic/models/music/album.dart'; 4 | import 'package:tearmusic/providers/theme_provider.dart'; 5 | import 'package:tearmusic/ui/mobile/common/views/album_view.dart'; 6 | import 'package:tearmusic/ui/mobile/common/cached_image.dart'; 7 | 8 | class SearchAlbumTile extends StatelessWidget { 9 | const SearchAlbumTile(this.album, {Key? key}) : super(key: key); 10 | 11 | final MusicAlbum album; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return ListTile( 16 | visualDensity: VisualDensity.compact, 17 | leading: SizedBox( 18 | width: 42, 19 | height: 42, 20 | child: CachedImage(album.images!), 21 | ), 22 | title: Text(album.name), 23 | subtitle: Text("${album.shortTitle} • ${album.releaseDate.year} • ${album.artists.first.name}"), 24 | onTap: () { 25 | FocusScope.of(context).requestFocus(FocusNode()); 26 | AlbumView.view(album, context: context).then((_) => context.read().resetTheme()); 27 | }, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/models/model.dart: -------------------------------------------------------------------------------- 1 | class Model { 2 | final Map? json; 3 | final String id; 4 | final String? key; 5 | final String type; 6 | 7 | Model({this.json, required this.id, required this.type, this.key}); 8 | 9 | static List encodeIdList(List models) => models.map((e) => "$e").toList(); 10 | 11 | bool match(String filter) { 12 | if (key == null || filter == "") return false; 13 | filter = filter.toLowerCase(); 14 | filter = SearchUtils.specialChars(filter); 15 | return filter.split(" ").every((variation) => SearchUtils.specialChars(key!.toLowerCase()).contains(variation)); 16 | } 17 | 18 | @override 19 | bool operator ==(other) => other is Model && other.id == id; 20 | 21 | @override 22 | int get hashCode => id.hashCode; 23 | 24 | @override 25 | String toString() => id; 26 | 27 | String get uri => "$type:$id"; 28 | } 29 | 30 | class SearchUtils { 31 | static String specialChars(String s) => s 32 | .replaceAll("é", "e") 33 | .replaceAll("á", "a") 34 | .replaceAll("ó", "o") 35 | .replaceAll("ő", "o") 36 | .replaceAll("ö", "o") 37 | .replaceAll("ú", "u") 38 | .replaceAll("ű", "u") 39 | .replaceAll("ü", "u") 40 | .replaceAll("í", "i"); 41 | } 42 | -------------------------------------------------------------------------------- /ios/Runner/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 860150371329-esqpv6qt4g9lvgiv2l5pk8a0j5a14mav.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.860150371329-esqpv6qt4g9lvgiv2l5pk8a0j5a14mav 9 | API_KEY 10 | AIzaSyCI70uiRvIhbBeKgZA6h6eIuxRAqFyeCdI 11 | GCM_SENDER_ID 12 | 860150371329 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | one.tear.tearmusic 17 | PROJECT_ID 18 | tear-music 19 | STORAGE_BUCKET 20 | tear-music.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:860150371329:ios:9430cbf0de56c4ec651c00 33 | 34 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/profile_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:tearmusic/providers/user_provider.dart'; 5 | import 'package:tearmusic/ui/mobile/screens/settings_screen.dart'; 6 | 7 | class ProfileButton extends StatelessWidget { 8 | const ProfileButton({Key? key}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final avatar = context.select((user) => user.avatar); 13 | 14 | onTap() { 15 | Navigator.of(context, rootNavigator: true).push( 16 | CupertinoPageRoute( 17 | builder: (context) => const SettingsScreen(), 18 | ), 19 | ); 20 | } 21 | 22 | return ClipOval( 23 | child: SizedBox( 24 | width: 36.0, 25 | height: 36.0, 26 | child: Stack( 27 | children: [ 28 | if (avatar != "") Image.network(avatar), 29 | Material( 30 | type: MaterialType.transparency, 31 | child: InkWell( 32 | onTap: onTap, 33 | customBorder: const CircleBorder(), 34 | ), 35 | ), 36 | ], 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/models/segmented.dart: -------------------------------------------------------------------------------- 1 | class Segmented { 2 | final Duration start; 3 | final Duration end; 4 | 5 | Segmented({ 6 | required this.start, 7 | required this.end, 8 | }); 9 | 10 | factory Segmented.decode(Map json) { 11 | return Segmented( 12 | start: Duration(milliseconds: (((json['start'] as num?)?.toDouble() ?? 0.0) * 1000).round()), 13 | end: Duration(milliseconds: (((json['end'] as num?)?.toDouble() ?? 0.0) * 1000).round()), 14 | ); 15 | } 16 | 17 | static List decodeList(List list) => list.map((e) => Segmented.decode(e)).toList().cast(); 18 | 19 | Duration get duration => end - start; 20 | } 21 | 22 | class TempoSegment extends Segmented { 23 | final double bpm; 24 | 25 | TempoSegment({ 26 | required this.bpm, 27 | required Duration start, 28 | required Duration end, 29 | }) : super(start: start, end: end); 30 | 31 | factory TempoSegment.decode(Map json) { 32 | final segment = Segmented.decode(json); 33 | return TempoSegment( 34 | bpm: (json['tempo'] as num?)?.toDouble() ?? 90.0, 35 | start: segment.start, 36 | end: segment.end, 37 | ); 38 | } 39 | 40 | static List decodeList(List list) => list.map((e) => TempoSegment.decode(e)).toList().cast(); 41 | } 42 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/tiles/search_playlist_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:tearmusic/models/music/playlist.dart'; 4 | import 'package:tearmusic/providers/theme_provider.dart'; 5 | import 'package:tearmusic/ui/mobile/common/cached_image.dart'; 6 | import 'package:tearmusic/ui/mobile/common/views/playlist_view.dart'; 7 | 8 | class SearchPlaylistTile extends StatelessWidget { 9 | const SearchPlaylistTile(this.playlist, {Key? key}) : super(key: key); 10 | 11 | final MusicPlaylist playlist; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return ListTile( 16 | visualDensity: VisualDensity.compact, 17 | leading: SizedBox( 18 | width: 42, 19 | height: 42, 20 | child: playlist.images != null ? CachedImage(playlist.images!) : null, 21 | ), 22 | title: Text( 23 | playlist.name, 24 | maxLines: 2, 25 | overflow: TextOverflow.ellipsis, 26 | ), 27 | subtitle: Text("${playlist.owner} • ${playlist.trackCount} songs"), 28 | onTap: () { 29 | FocusScope.of(context).requestFocus(FocusNode()); 30 | PlaylistView.view(playlist, context: context).then((_) => context.read().resetTheme()); 31 | }, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/providers/current_music_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:tearmusic/models/music/track.dart'; 5 | import 'package:tearmusic/models/playback.dart'; 6 | import 'package:tearmusic/providers/audio_stream_provider.dart'; 7 | 8 | class Audio { 9 | final Completer playback; 10 | 11 | Audio(this.playback); 12 | } 13 | 14 | enum AudioLoadingState { ready, loading, error } 15 | 16 | class CurrentMusicProvider extends ChangeNotifier { 17 | get playing => null; 18 | double get progress => 0.0; 19 | Duration get position => Duration.zero; 20 | Stream get positionStream => Stream.value(position); 21 | bool get isPlaying => false; 22 | Stream get isPlayingStream => Stream.value(isPlaying); 23 | Duration? get duration => Duration.zero; 24 | Audio? get tma => Audio(Completer()); 25 | AudioLoadingState get audioLoading => AudioLoadingState.ready; 26 | 27 | Future init() async {} 28 | 29 | void play() {} 30 | 31 | Future playTrack(MusicTrack track) async { 32 | final stream = AudioStreamProvider(); 33 | await stream.startServer(); 34 | await Future.delayed(const Duration(seconds: 3)); 35 | await stream.stopServer(); 36 | } 37 | 38 | Future seek(Duration position) async {} 39 | 40 | Future pause() async {} 41 | } 42 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/settings/settings_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SettingsContainer extends StatelessWidget { 4 | const SettingsContainer({ 5 | Key? key, 6 | required this.name, 7 | required this.items, 8 | }) : super(key: key); 9 | 10 | final String name; 11 | final List items; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Padding( 16 | padding: const EdgeInsets.symmetric(vertical: 6.0), 17 | child: Card( 18 | elevation: 3.0, 19 | child: Column( 20 | mainAxisSize: MainAxisSize.min, 21 | children: [ 22 | Padding( 23 | padding: const EdgeInsets.only(left: 16.0, right: 8.0), 24 | child: Row( 25 | children: [ 26 | Expanded( 27 | child: Text( 28 | name, 29 | style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0), 30 | ), 31 | ), 32 | TextButton( 33 | onPressed: () {}, 34 | child: const Text("Restore default"), 35 | ) 36 | ], 37 | ), 38 | ), 39 | ...items, 40 | ], 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/models/playback.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:tearmusic/models/segmented.dart'; 3 | 4 | class PlaybackHead { 5 | final Uint8List prefetch; 6 | final List silence; 7 | final List tempo; 8 | final int sourceLength; 9 | 10 | PlaybackHead({ 11 | required this.prefetch, 12 | required this.silence, 13 | required this.tempo, 14 | required this.sourceLength, 15 | }); 16 | 17 | factory PlaybackHead.decode(Object? data) { 18 | final json = (data as Map); 19 | return PlaybackHead( 20 | prefetch: json['buffer'].buffer.asUint8List(), 21 | silence: Segmented.decodeList((json['silence'] as List).cast()), 22 | tempo: TempoSegment.decodeList((json['tempo'] as List).cast()), 23 | sourceLength: json['sourceLength'], 24 | ); 25 | } 26 | } 27 | 28 | class Playback { 29 | final String streamUrl; 30 | final List waveform; 31 | final List silence; 32 | 33 | Playback({ 34 | required this.streamUrl, 35 | required this.waveform, 36 | required this.silence, 37 | }); 38 | 39 | factory Playback.decode(Map json) { 40 | return Playback( 41 | streamUrl: json['cdn'], 42 | waveform: (json['proc']['waveform'] as List).cast().map((e) => e?.toDouble() ?? 0.0).toList(), // yes this is needed 43 | silence: Segmented.decodeList((json['proc']['silence'] as List).cast()), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/knob.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class Knob extends StatelessWidget { 6 | const Knob({Key? key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Align( 11 | alignment: Alignment.topCenter, 12 | child: Container( 13 | height: 64, 14 | width: double.infinity, 15 | alignment: Alignment.topCenter, 16 | decoration: BoxDecoration( 17 | gradient: LinearGradient( 18 | begin: Alignment.topCenter, 19 | end: Alignment.bottomCenter, 20 | colors: [ 21 | Colors.black.withOpacity(.1), 22 | Colors.black.withOpacity(0), 23 | ], 24 | ), 25 | ), 26 | child: Padding( 27 | padding: const EdgeInsets.all(24.0), 28 | child: SizedBox( 29 | height: 6.0, 30 | width: 52.0, 31 | child: ClipRRect( 32 | borderRadius: BorderRadius.circular(45.0), 33 | child: BackdropFilter( 34 | filter: ImageFilter.blur( 35 | sigmaX: 24.0, 36 | sigmaY: 24.0, 37 | ), 38 | child: Container( 39 | color: Theme.of(context).colorScheme.onSurface.withOpacity(.25), 40 | ), 41 | ), 42 | ), 43 | ), 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/view_menu_button.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; 6 | 7 | class ViewMenuButton extends StatelessWidget { 8 | const ViewMenuButton({Key? key}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return ClipOval( 13 | child: SizedBox( 14 | height: 34, 15 | width: 34, 16 | child: BackdropFilter( 17 | filter: ImageFilter.blur( 18 | sigmaX: 12.0, 19 | sigmaY: 12.0, 20 | ), 21 | child: IconButton( 22 | padding: EdgeInsets.zero, 23 | onPressed: () { 24 | showMaterialModalBottomSheet( 25 | context: context, 26 | useRootNavigator: true, 27 | builder: (context) => Container(height: 300), 28 | ); 29 | }, 30 | icon: Container( 31 | padding: const EdgeInsets.all(4.0), 32 | decoration: BoxDecoration( 33 | color: Theme.of(context).colorScheme.secondary.withOpacity(.3), 34 | shape: BoxShape.circle, 35 | ), 36 | child: Icon(CupertinoIcons.ellipsis, color: Theme.of(context).colorScheme.secondary), 37 | ), 38 | iconSize: 26.0, 39 | ), 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/menu_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MenuButton extends StatelessWidget { 4 | const MenuButton({Key? key, this.child, this.onPressed, this.icon}) : super(key: key); 5 | 6 | final Widget? child; 7 | final Widget? icon; 8 | final Function()? onPressed; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return InkWell( 13 | borderRadius: BorderRadius.circular(12.0), 14 | onTap: onPressed, 15 | child: Padding( 16 | padding: const EdgeInsets.symmetric(vertical: 14.0, horizontal: 12.0), 17 | child: Row( 18 | children: [ 19 | if (icon != null) 20 | IconTheme( 21 | data: IconThemeData( 22 | color: Theme.of(context).colorScheme.secondary, 23 | size: 24.0, 24 | ), 25 | child: icon!, 26 | ), 27 | if (child != null) 28 | Expanded( 29 | child: Padding( 30 | padding: const EdgeInsets.only(left: 24.0), 31 | child: DefaultTextStyle( 32 | style: Theme.of(context).textTheme.bodyMedium!.copyWith( 33 | fontWeight: FontWeight.w500, 34 | fontSize: 16.0, 35 | ), 36 | child: child!, 37 | ), 38 | ), 39 | ), 40 | ], 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/models/music/images.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:tearmusic/models/model.dart'; 3 | 4 | class Images { 5 | final List _internal; 6 | 7 | Images({required List images}) : _internal = images { 8 | _internal.sort((a, b) => (a.height * a.width).compareTo(b.height * b.width)); 9 | assert(_internal.isNotEmpty); 10 | } 11 | 12 | factory Images.decode(List json) { 13 | return Images( 14 | images: json.map((e) => InternalImage.decode(e)).toList(), 15 | ); 16 | } 17 | 18 | List encode() => _internal.map((e) => e.encode()).toList(); 19 | 20 | String forSize(Size size) => _internal 21 | .firstWhere( 22 | (e) => e.width > size.width || e.height > size.height, 23 | orElse: () => _internal.last, 24 | ) 25 | .url; 26 | InternalImage get maxSize => _internal.last; 27 | InternalImage get minSize => _internal.first; 28 | } 29 | 30 | class InternalImage extends Model { 31 | final String url; 32 | final int width; 33 | final int height; 34 | 35 | InternalImage({ 36 | required Map json, 37 | required this.url, 38 | required this.width, 39 | required this.height, 40 | }) : super(id: "${width}_${height}_$url", json: json, type: "image"); 41 | 42 | factory InternalImage.decode(Map json) { 43 | return InternalImage( 44 | json: json, 45 | url: json["url"], 46 | width: json["width"] ?? 0, 47 | height: json["height"] ?? 0, 48 | ); 49 | } 50 | 51 | Map encode() => json ?? {}; 52 | } 53 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | const BoxShadow _kDefaultBoxShadow = BoxShadow(blurRadius: 10, color: Colors.black12, spreadRadius: 5); 5 | const double _kPreviousPageVisibleOffset = 10; 6 | 7 | class BottomSheetContainer extends StatelessWidget { 8 | final Widget child; 9 | final Color? backgroundColor; 10 | final Radius topRadius; 11 | final BoxShadow? shadow; 12 | 13 | const BottomSheetContainer({ 14 | Key? key, 15 | required this.child, 16 | this.backgroundColor, 17 | required this.topRadius, 18 | this.shadow, 19 | }) : super(key: key); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final topSafeAreaPadding = MediaQuery.of(context).padding.top; 24 | final topPadding = _kPreviousPageVisibleOffset + topSafeAreaPadding; 25 | 26 | final shadowOrDefault = shadow ?? _kDefaultBoxShadow; 27 | final backgroundOrDefault = backgroundColor ?? CupertinoTheme.of(context).scaffoldBackgroundColor; 28 | return Padding( 29 | padding: EdgeInsets.only(top: topPadding), 30 | child: ClipRRect( 31 | borderRadius: BorderRadius.vertical(top: topRadius), 32 | child: Container( 33 | decoration: BoxDecoration(color: backgroundOrDefault, boxShadow: [shadowOrDefault]), 34 | width: double.infinity, 35 | child: MediaQuery.removePadding( 36 | context: context, 37 | removeTop: true, //Remove top Safe Area 38 | child: child, 39 | ), 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/views/artist_view/artist_header_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ArtistHeaderButton extends StatelessWidget { 4 | const ArtistHeaderButton({Key? key, this.icon, this.onPressed, required this.child}) : super(key: key); 5 | 6 | final Function()? onPressed; 7 | final Widget? icon; 8 | final Widget child; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return InkWell( 13 | onTap: onPressed, 14 | borderRadius: BorderRadius.circular(12.0), 15 | child: Material( 16 | color: Theme.of(context).colorScheme.primary, 17 | borderRadius: BorderRadius.circular(12.0), 18 | child: Padding( 19 | padding: const EdgeInsets.all(14.0), 20 | child: Row( 21 | mainAxisAlignment: MainAxisAlignment.center, 22 | children: [ 23 | if (icon != null) 24 | Padding( 25 | padding: const EdgeInsets.only(right: 8.0), 26 | child: IconTheme( 27 | data: IconThemeData( 28 | color: Theme.of(context).colorScheme.onPrimary, 29 | size: 20.0, 30 | ), 31 | child: icon!, 32 | ), 33 | ), 34 | DefaultTextStyle( 35 | style: Theme.of(context).textTheme.bodyMedium!.copyWith( 36 | fontWeight: FontWeight.w700, 37 | color: Theme.of(context).colorScheme.onPrimary, 38 | ), 39 | child: child, 40 | ), 41 | ], 42 | ), 43 | ), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/player/lyrics_view/unavailable.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LyricsUnavailalbe extends StatelessWidget { 4 | const LyricsUnavailalbe({Key? key}) : super(key: key); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Center( 9 | child: Column( 10 | children: [ 11 | const Text( 12 | "🫤", 13 | style: TextStyle( 14 | fontSize: 64.0, 15 | ), 16 | ), 17 | const Padding( 18 | padding: EdgeInsets.only(top: 12.0), 19 | child: Text( 20 | "Sorry, no lyrics...", 21 | style: TextStyle( 22 | fontSize: 32.0, 23 | fontWeight: FontWeight.bold, 24 | ), 25 | ), 26 | ), 27 | Padding( 28 | padding: const EdgeInsets.only(top: 100.0), 29 | child: IconButton( 30 | icon: const Icon(Icons.arrow_back), 31 | iconSize: 32.0, 32 | padding: const EdgeInsets.all(12.0), 33 | style: ButtonStyle( 34 | backgroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.secondaryContainer), 35 | foregroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.onSecondaryContainer), 36 | ), 37 | onPressed: () { 38 | Navigator.of(context, rootNavigator: true).pop(); 39 | }, 40 | ), 41 | ), 42 | const Padding( 43 | padding: EdgeInsets.only(top: 12.0), 44 | child: Text("Back"), 45 | ), 46 | ], 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | 41 | target.build_configurations.each do |config| 42 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 43 | '$(inherited)', 44 | 'AUDIO_SESSION_MICROPHONE=0' 45 | ] 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/tiles/search_artist_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:tearmusic/models/music/artist.dart'; 4 | import 'package:tearmusic/providers/theme_provider.dart'; 5 | import 'package:tearmusic/ui/mobile/common/cached_image.dart'; 6 | import 'package:tearmusic/ui/mobile/common/views/artist_view/artist_view.dart'; 7 | 8 | class SearchArtistTile extends StatelessWidget { 9 | const SearchArtistTile(this.artist, {Key? key}) : super(key: key); 10 | 11 | final MusicArtist artist; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return ListTile( 16 | visualDensity: VisualDensity.compact, 17 | leading: artist.images != null 18 | ? SizedBox( 19 | width: 42, 20 | height: 42, 21 | child: CachedImage(artist.images!, borderRadius: 45.0), 22 | ) 23 | : Container( 24 | width: 42, 25 | height: 42, 26 | decoration: BoxDecoration( 27 | shape: BoxShape.circle, 28 | color: Theme.of(context).colorScheme.surfaceVariant, 29 | ), 30 | child: Center( 31 | child: Icon( 32 | Icons.person, 33 | color: Theme.of(context).colorScheme.secondary, 34 | ), 35 | ), 36 | ), 37 | title: Text(artist.name), 38 | subtitle: artist.genres.isNotEmpty ? Text(artist.genres.first) : null, 39 | onTap: () { 40 | FocusScope.of(context).requestFocus(FocusNode()); 41 | ArtistView.view(artist, context: context).then((_) => context.read().resetTheme()); 42 | }, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/tiles/manual_match_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tearmusic/models/manual_match.dart'; 3 | import 'package:tearmusic/ui/common/format.dart'; 4 | 5 | class ManualMatchTile extends StatelessWidget { 6 | const ManualMatchTile(this.match, {Key? key, this.onTap, this.selected = false}) : super(key: key); 7 | 8 | final ManualMatch match; 9 | final void Function()? onTap; 10 | final bool selected; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return ListTile( 15 | visualDensity: VisualDensity.compact, 16 | contentPadding: EdgeInsets.zero, 17 | leading: Stack( 18 | children: [ 19 | ClipRRect( 20 | borderRadius: BorderRadius.circular(4.0), 21 | child: SizedBox( 22 | height: 42.0, 23 | width: 42.0, 24 | child: Image.network( 25 | match.imageUrl, 26 | fit: BoxFit.cover, 27 | ), 28 | ), 29 | ), 30 | if (selected) 31 | Container( 32 | height: 42.0, 33 | width: 42.0, 34 | color: Colors.black.withOpacity(.5), 35 | child: const Center( 36 | child: Icon(Icons.check), 37 | ), 38 | ), 39 | ], 40 | ), 41 | title: Text( 42 | match.name, 43 | maxLines: 2, 44 | overflow: TextOverflow.ellipsis, 45 | style: selected ? const TextStyle(fontWeight: FontWeight.bold) : null, 46 | ), 47 | subtitle: Text( 48 | match.artist, 49 | maxLines: 1, 50 | overflow: TextOverflow.ellipsis, 51 | ), 52 | trailing: Text(match.duration.shortFormat()), 53 | onTap: onTap, 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Tear Music 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | tearmusic 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | UIApplicationSupportsIndirectInputEvents 35 | 36 | UIBackgroundModes 37 | 38 | audio 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIMainStoryboardFile 43 | Main 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | 48 | UISupportedInterfaceOrientations~ipad 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationPortraitUpsideDown 52 | 53 | UIViewControllerBasedStatusBarAppearance 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /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 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/models/music/artist.dart: -------------------------------------------------------------------------------- 1 | import 'package:tearmusic/models/music/album.dart'; 2 | import 'package:tearmusic/models/music/images.dart'; 3 | import 'package:tearmusic/models/model.dart'; 4 | import 'package:tearmusic/models/music/track.dart'; 5 | 6 | class MusicArtist extends Model { 7 | final String name; 8 | final List genres; 9 | final Images? images; 10 | final int followers; 11 | 12 | MusicArtist({ 13 | required Map json, 14 | required String id, 15 | required this.name, 16 | required this.genres, 17 | required this.images, 18 | required this.followers, 19 | }) : super(id: id, json: json, key: name, type: "artist"); 20 | 21 | factory MusicArtist.decode(Map json) { 22 | final images = json["images"] as List?; 23 | return MusicArtist( 24 | json: json, 25 | id: json["id"], 26 | name: json["name"], 27 | genres: ((json["genres"] as List?) ?? []).cast(), 28 | images: images != null && images.isNotEmpty ? Images.decode(images.cast()) : null, 29 | followers: json["followers"] ?? 0, 30 | ); 31 | } 32 | 33 | Map encode() => json ?? {}; 34 | 35 | static List decodeList(List encoded) => encoded 36 | .where((e) => e["id"] != null && e["images"] != null && e["images"].isNotEmpty) 37 | .map((e) => MusicArtist.decode(e)) 38 | .toList() 39 | .cast(); 40 | static List encodeList(List models) => models.map((e) => e.encode()).toList().cast(); 41 | } 42 | 43 | class ArtistDetails { 44 | final MusicArtist artist; 45 | final List tracks; 46 | final List albums; 47 | final List related; 48 | final List appearsOn; 49 | 50 | ArtistDetails({ 51 | required this.artist, 52 | required this.tracks, 53 | required this.albums, 54 | required this.related, 55 | required this.appearsOn, 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /lib/models/music/track.dart: -------------------------------------------------------------------------------- 1 | import 'package:tearmusic/models/music/album.dart'; 2 | import 'package:tearmusic/models/music/artist.dart'; 3 | import 'package:tearmusic/models/model.dart'; 4 | 5 | class MusicTrack extends Model { 6 | final String name; 7 | final Duration duration; 8 | final bool explicit; 9 | final int trackNumber; 10 | final MusicAlbum? album; 11 | final List artists; 12 | 13 | MusicTrack({ 14 | required Map json, 15 | required String id, 16 | required this.name, 17 | required this.duration, 18 | required this.explicit, 19 | required this.trackNumber, 20 | required this.album, 21 | required this.artists, 22 | }) : super(id: id, json: json, key: "$name ${artists.first.name}", type: "track"); 23 | 24 | factory MusicTrack.decode(Map json, {MusicAlbum? album}) { 25 | if (album != null) json['album'] = album.json; 26 | 27 | return MusicTrack( 28 | json: json, 29 | id: json["id"] ?? "", 30 | name: json["name"], 31 | duration: Duration(milliseconds: json["duration_ms"]), 32 | explicit: json["explicit"], 33 | trackNumber: json["track_number"], 34 | album: album ?? (json["album"] != null ? MusicAlbum.decode(json["album"]) : null), 35 | artists: json["artists"].map((e) => MusicArtist.decode(e)).toList().cast(), 36 | ); 37 | } 38 | 39 | Map encode() => json ?? {}; 40 | 41 | static List decodeList(List encoded, {MusicAlbum? album}) => 42 | encoded.where((e) => e["id"] != null).map((e) => MusicTrack.decode(e, album: album)).toList().cast(); 43 | static List encodeList(List models) => models.map((e) => e.encode()).toList().cast(); 44 | 45 | String get artistsLabel { 46 | if (artists.length == 2) { 47 | return "${artists[0].name} & ${artists[1].name}"; 48 | } 49 | return artists.map((e) => e.name).join(", "); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/ui/mobile/pages/library/artist_loading_tile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:shimmer/shimmer.dart'; 5 | 6 | class ArtistLoadingTile extends StatelessWidget { 7 | const ArtistLoadingTile({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Padding( 12 | padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8.0, top: 2.0), 13 | child: Shimmer.fromColors( 14 | baseColor: Colors.white.withOpacity(.05), 15 | highlightColor: Colors.white.withOpacity(.25), 16 | child: ListView.builder( 17 | padding: EdgeInsets.zero, 18 | scrollDirection: Axis.horizontal, 19 | physics: const NeverScrollableScrollPhysics(), 20 | shrinkWrap: true, 21 | itemCount: 3, 22 | itemBuilder: (_, int index) => Padding( 23 | padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), 24 | child: Row( 25 | children: [ 26 | Column( 27 | children: [ 28 | Container( 29 | width: 100.0, 30 | height: 100.0, 31 | decoration: const BoxDecoration( 32 | color: Colors.white, 33 | shape: BoxShape.circle, 34 | ), 35 | ), 36 | const SizedBox(height: 6), 37 | Container( 38 | width: Random().nextInt(25) + 75, 39 | height: 18.0, 40 | decoration: BoxDecoration( 41 | color: Colors.white, 42 | borderRadius: BorderRadius.circular(12.0), 43 | ), 44 | ), 45 | ], 46 | ), 47 | ], 48 | ), 49 | ), 50 | ), 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | tearmusic 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/tiles/artist_artist_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tearmusic/models/music/artist.dart'; 3 | import 'package:tearmusic/ui/mobile/common/cached_image.dart'; 4 | import 'package:tearmusic/ui/mobile/common/views/artist_view/artist_view.dart'; 5 | 6 | class ArtistArtistTile extends StatelessWidget { 7 | const ArtistArtistTile(this.artist, {Key? key, this.then}) : super(key: key); 8 | 9 | final MusicArtist artist; 10 | final void Function()? then; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return GestureDetector( 15 | onTap: () { 16 | ArtistView.view(artist, context: context).then((_) => then != null ? then!() : null); 17 | }, 18 | child: SizedBox( 19 | width: 100, 20 | child: Column( 21 | mainAxisSize: MainAxisSize.min, 22 | children: [ 23 | SizedBox( 24 | width: 100, 25 | height: 100, 26 | child: ClipOval( 27 | child: Stack( 28 | children: [ 29 | CachedImage(artist.images!), 30 | Material( 31 | type: MaterialType.transparency, 32 | child: InkWell( 33 | onTap: () { 34 | ArtistView.view(artist, context: context).then((_) => then != null ? then!() : null); 35 | }, 36 | ), 37 | ), 38 | ], 39 | ), 40 | ), 41 | ), 42 | Padding( 43 | padding: const EdgeInsets.only(top: 6.0), 44 | child: Text( 45 | artist.name, 46 | maxLines: 2, 47 | overflow: TextOverflow.ellipsis, 48 | textAlign: TextAlign.center, 49 | style: const TextStyle( 50 | fontWeight: FontWeight.w500, 51 | ), 52 | ), 53 | ), 54 | ], 55 | ), 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/settings/settings_switch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SettingsSwitchTile extends StatelessWidget { 4 | const SettingsSwitchTile({ 5 | Key? key, 6 | required this.name, 7 | required this.desc, 8 | required this.value, 9 | required this.onChanged, 10 | }) : super(key: key); 11 | 12 | final String name; 13 | final String desc; 14 | final bool value; 15 | final Function(bool) onChanged; 16 | 17 | Color? Function(Set) switchColor(BuildContext context, double opacity) { 18 | return (states) { 19 | if (states.contains(MaterialState.selected)) { 20 | return Theme.of(context).colorScheme.primary.withOpacity(opacity); 21 | } else { 22 | return Theme.of(context).colorScheme.secondary.withOpacity(opacity); 23 | } 24 | }; 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return SwitchTheme( 30 | data: SwitchThemeData( 31 | thumbColor: MaterialStateProperty.resolveWith(switchColor(context, 1.0)), 32 | trackColor: MaterialStateProperty.resolveWith(switchColor(context, 0.5)), 33 | ), 34 | child: Padding( 35 | padding: const EdgeInsets.symmetric(vertical: 4.0), 36 | child: Row( 37 | children: [ 38 | Expanded( 39 | child: Padding( 40 | padding: const EdgeInsets.only(left: 16.0, bottom: 6.0), 41 | child: Column( 42 | crossAxisAlignment: CrossAxisAlignment.start, 43 | children: [ 44 | Text( 45 | name, 46 | style: const TextStyle(fontWeight: FontWeight.w400, fontSize: 15.0), 47 | ), 48 | Text( 49 | desc, 50 | style: const TextStyle(fontWeight: FontWeight.w300, fontSize: 13.0, color: Colors.grey), 51 | ), 52 | ], 53 | ), 54 | ), 55 | ), 56 | Switch(value: value, onChanged: onChanged), 57 | ], 58 | ), 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/models/music/playlist.dart: -------------------------------------------------------------------------------- 1 | import 'package:tearmusic/models/music/images.dart'; 2 | import 'package:tearmusic/models/model.dart'; 3 | import 'package:tearmusic/models/music/track.dart'; 4 | 5 | class MusicPlaylist extends Model { 6 | final String name; 7 | final String description; 8 | final Images? images; 9 | final int trackCount; 10 | final String owner; 11 | 12 | MusicPlaylist({ 13 | required Map json, 14 | required String id, 15 | required this.name, 16 | required this.description, 17 | required this.images, 18 | required this.trackCount, 19 | required this.owner, 20 | }) : super(id: id, json: json, key: name, type: "playlist"); 21 | 22 | factory MusicPlaylist.decode(Map json) { 23 | final images = json["images"] as List?; 24 | return MusicPlaylist( 25 | json: json, 26 | id: json["id"], 27 | name: json["name"], 28 | description: json["description"], 29 | images: images != null && images.isNotEmpty ? Images.decode(images.cast()) : null, 30 | trackCount: json["track_count"] ?? 0, 31 | owner: json["owner"] != null ? json["owner"]["name"] : "", 32 | ); 33 | } 34 | 35 | Map encode() => json ?? {}; 36 | 37 | static List decodeList(List encoded) => 38 | encoded.where((e) => e["id"] != null).map((e) => MusicPlaylist.decode(e)).toList().cast(); 39 | static List encodeList(List models) => models.map((e) => e.encode()).toList().cast(); 40 | } 41 | 42 | class PlaylistDetails extends Model { 43 | final List tracks; 44 | final int followers; 45 | 46 | PlaylistDetails({ 47 | required Map json, 48 | required String id, 49 | required this.tracks, 50 | required this.followers, 51 | }) : super(id: id, json: json, type: "playlistextras"); 52 | 53 | factory PlaylistDetails.decode(Map json) { 54 | final tracks = MusicTrack.decodeList((json['tracks'] as List).cast()); 55 | return PlaylistDetails( 56 | json: json, 57 | id: tracks.map((e) => e.id).join(","), 58 | tracks: tracks, 59 | followers: json['followers'] ?? 0, 60 | ); 61 | } 62 | 63 | Map encode() => json ?? {}; 64 | } 65 | -------------------------------------------------------------------------------- /lib/models/library.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: non_constant_identifier_names, constant_identifier_names 2 | 3 | class UserLibrary { 4 | List liked_tracks = []; 5 | List liked_artists = []; 6 | List liked_albums = []; 7 | List liked_playlists = []; 8 | List track_history = []; 9 | 10 | UserLibrary({ 11 | required this.liked_tracks, 12 | required this.liked_artists, 13 | required this.liked_albums, 14 | required this.liked_playlists, 15 | required this.track_history, 16 | }); 17 | 18 | factory UserLibrary.decode(Map json) { 19 | return UserLibrary( 20 | liked_tracks: (json["liked_tracks"] as List).cast(), 21 | liked_artists: (json["liked_artists"] as List).cast(), 22 | liked_albums: (json["liked_albums"] as List).cast(), 23 | liked_playlists: (json["liked_playlists"] as List).cast(), 24 | track_history: (json["track_history"] as List).map((e) => UserTrackHistory.decode(e)).toList(), 25 | ); 26 | } 27 | 28 | Map encode() { 29 | //print("encoding ${track_history.map((e) => e.encode())}"); 30 | return { 31 | "liked_tracks": liked_tracks, 32 | "liked_artists": liked_artists, 33 | "liked_albums": liked_albums, 34 | "liked_playlists": liked_playlists, 35 | "track_history": track_history.map((e) => e.encode()).toList(), 36 | }; 37 | } 38 | } 39 | 40 | enum LibraryType { liked_tracks, liked_artists, liked_albums, liked_playlists, track_history } 41 | 42 | class UserTrackHistory { 43 | int date; 44 | String track_id; 45 | String? from_id; 46 | String? from_type; 47 | 48 | UserTrackHistory({ 49 | required this.date, 50 | required this.track_id, 51 | this.from_id, 52 | this.from_type, 53 | }); 54 | 55 | factory UserTrackHistory.decode(Map json) { 56 | return UserTrackHistory( 57 | date: json["date"], 58 | track_id: json["track_id"], 59 | from_id: json["from_id"], 60 | from_type: json["from_type"], 61 | ); 62 | } 63 | 64 | Map encode() => { 65 | "date": date, 66 | "track_id": track_id, 67 | "from_id": from_id, 68 | "from_type": from_type, 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /lib/ui/mobile/pages/search/top_result_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TopResultContainer extends StatelessWidget { 4 | const TopResultContainer({ 5 | Key? key, 6 | this.icon, 7 | required this.results, 8 | required this.kind, 9 | required this.index, 10 | required this.tabController, 11 | required this.pageController, 12 | }) : super(key: key); 13 | 14 | final List results; 15 | final String kind; 16 | final int index; 17 | final TabController tabController; 18 | final PageController pageController; 19 | final IconData? icon; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | if (results.isEmpty) return const SizedBox(); 24 | 25 | return Padding( 26 | padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), 27 | child: Card( 28 | elevation: 2.0, 29 | child: Column( 30 | mainAxisSize: MainAxisSize.min, 31 | children: [ 32 | Padding( 33 | padding: const EdgeInsets.only(left: 14.0, right: 8.0), 34 | child: Row( 35 | children: [ 36 | if (icon != null) 37 | Padding( 38 | padding: const EdgeInsets.only(right: 8.0), 39 | child: Icon( 40 | icon, 41 | size: 20.0, 42 | ), 43 | ), 44 | Expanded( 45 | child: Text( 46 | "Top $kind", 47 | style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0), 48 | ), 49 | ), 50 | TextButton( 51 | onPressed: () { 52 | tabController.animateTo(index); 53 | pageController.animateToPage(index, curve: Curves.easeIn, duration: kTabScrollDuration); 54 | }, 55 | child: const Text("Show All"), 56 | ) 57 | ], 58 | ), 59 | ), 60 | ...results, 61 | ], 62 | ), 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/providers/theme_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tearmusic/ui/mobile/navigator.dart'; 3 | 4 | class ThemePageState { 5 | ThemeData? _appThemeB; 6 | ThemeData? _navigationTheme; 7 | Color? _key; 8 | } 9 | 10 | class ThemeProvider extends ChangeNotifier { 11 | static final defaultTheme = coloredTheme(Colors.blue); 12 | 13 | MobileRoute? _currentRoute; 14 | final Map _pages = {}; 15 | ThemePageState get _defaultPlaceholderPage => ThemePageState(); 16 | ThemePageState get _currentPage => _pages[_currentRoute] ?? _defaultPlaceholderPage; 17 | 18 | ThemeData? get _appThemeB => _currentPage._appThemeB; 19 | set _appThemeB(value) => _currentPage._appThemeB = value; 20 | ThemeData? get _navigationTheme => _currentPage._navigationTheme; 21 | set _navigationTheme(value) => _currentPage._navigationTheme = value; 22 | Color? get _key => _currentPage._key; 23 | set _key(value) => _currentPage._key = value; 24 | 25 | late ThemeData _appThemeA; 26 | ThemeData get appTheme => _appThemeB ?? _appThemeA; 27 | ThemeData get navigationTheme => _navigationTheme ?? _appThemeA; 28 | Color get key => _key ?? Colors.blue; 29 | 30 | ThemeProvider() { 31 | setTheme(defaultTheme); 32 | } 33 | 34 | void setState(MobileRoute route) { 35 | if (!_pages.keys.contains(route)) { 36 | _pages[route] = ThemePageState(); 37 | } 38 | } 39 | 40 | void restoreState(MobileRoute route) { 41 | _currentRoute = route; 42 | } 43 | 44 | void tempAppTheme(ThemeData theme) { 45 | _appThemeB = theme; 46 | notifyListeners(); 47 | } 48 | 49 | void tempNavTheme(ThemeData theme) { 50 | _navigationTheme = theme; 51 | notifyListeners(); 52 | } 53 | 54 | void resetTheme() { 55 | _appThemeB = null; 56 | _navigationTheme = null; 57 | notifyListeners(); 58 | } 59 | 60 | void setTheme(ThemeData theme) { 61 | _appThemeA = theme; 62 | notifyListeners(); 63 | } 64 | 65 | void setThemeKey(Color color) { 66 | _key = color; 67 | setTheme(coloredTheme(color)); 68 | } 69 | 70 | static ThemeData coloredTheme(Color color) { 71 | return ThemeData( 72 | useMaterial3: true, 73 | colorSchemeSeed: color, 74 | brightness: Brightness.dark, 75 | fontFamily: "Montserrat", 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/ui/mobile/pages/library/album_loading_tile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:shimmer/shimmer.dart'; 5 | 6 | class AlbumLoadingTile extends StatelessWidget { 7 | const AlbumLoadingTile({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Padding( 12 | padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8.0, top: 2.0), 13 | child: Shimmer.fromColors( 14 | baseColor: Colors.white.withOpacity(.05), 15 | highlightColor: Colors.white.withOpacity(.25), 16 | child: ListView.builder( 17 | padding: EdgeInsets.zero, 18 | scrollDirection: Axis.horizontal, 19 | physics: const NeverScrollableScrollPhysics(), 20 | shrinkWrap: true, 21 | itemCount: 3, 22 | itemBuilder: (_, int index) => Padding( 23 | padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), 24 | child: Row( 25 | children: [ 26 | Column( 27 | crossAxisAlignment: CrossAxisAlignment.start, 28 | children: [ 29 | Container( 30 | width: 110.0, 31 | height: 110.0, 32 | decoration: BoxDecoration( 33 | color: Colors.white, 34 | borderRadius: BorderRadius.circular(4.0), 35 | ), 36 | ), 37 | const SizedBox(height: 8.0), 38 | Container( 39 | width: 110, 40 | height: 18.0, 41 | decoration: BoxDecoration( 42 | color: Colors.white, 43 | borderRadius: BorderRadius.circular(12.0), 44 | ), 45 | ), 46 | const SizedBox(height: 4.0), 47 | Container( 48 | width: Random().nextInt(50) + 50, 49 | height: 18.0, 50 | decoration: BoxDecoration( 51 | color: Colors.white, 52 | borderRadius: BorderRadius.circular(12.0), 53 | ), 54 | ), 55 | ], 56 | ), 57 | ], 58 | ), 59 | ), 60 | ), 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/tiles/artist_album_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tearmusic/models/music/album.dart'; 3 | import 'package:tearmusic/ui/mobile/common/cached_image.dart'; 4 | import 'package:tearmusic/ui/mobile/common/views/album_view.dart'; 5 | 6 | class ArtistAlbumTile extends StatelessWidget { 7 | const ArtistAlbumTile(this.album, {Key? key, this.then, this.size = 130}) : super(key: key); 8 | 9 | const ArtistAlbumTile.small(this.album, {Key? key, this.then, this.size = 110}) : super(key: key); 10 | 11 | final MusicAlbum album; 12 | final void Function()? then; 13 | 14 | final double size; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return SizedBox( 19 | width: size, 20 | child: GestureDetector( 21 | onTap: () { 22 | AlbumView.view(album, context: context).then((_) => then != null ? then!() : null); 23 | }, 24 | child: Column( 25 | mainAxisSize: MainAxisSize.min, 26 | crossAxisAlignment: CrossAxisAlignment.start, 27 | children: [ 28 | SizedBox( 29 | width: size, 30 | height: size, 31 | child: Stack( 32 | children: [ 33 | CachedImage(album.images!), 34 | Material( 35 | type: MaterialType.transparency, 36 | child: InkWell( 37 | onTap: () { 38 | AlbumView.view(album, context: context).then((_) => then != null ? then!() : null); 39 | }, 40 | ), 41 | ), 42 | ], 43 | ), 44 | ), 45 | Padding( 46 | padding: const EdgeInsets.only(top: 4.0), 47 | child: Text( 48 | album.name, 49 | maxLines: 2, 50 | overflow: TextOverflow.ellipsis, 51 | style: TextStyle(fontWeight: FontWeight.w500, fontSize: size / 10 + 1), 52 | ), 53 | ), 54 | Text( 55 | "${album.shortTitle} • ${album.releaseDate.year}", 56 | maxLines: 1, 57 | overflow: TextOverflow.ellipsis, 58 | style: TextStyle( 59 | color: Theme.of(context).colorScheme.secondary.withOpacity(.8), 60 | fontSize: size / 10 + 1, 61 | ), 62 | ), 63 | ], 64 | ), 65 | ), 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/views/artist_view/latest_release.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tearmusic/models/music/album.dart'; 3 | import 'package:tearmusic/ui/mobile/common/views/album_view.dart'; 4 | import 'package:tearmusic/ui/mobile/common/cached_image.dart'; 5 | 6 | class LatestRelease extends StatelessWidget { 7 | const LatestRelease(this.album, {Key? key, this.then}) : super(key: key); 8 | 9 | final MusicAlbum album; 10 | final void Function()? then; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Card( 15 | elevation: 1.0, 16 | child: InkWell( 17 | onTap: () { 18 | AlbumView.view(album, context: context).then((_) => then != null ? then!() : null); 19 | }, 20 | child: Padding( 21 | padding: const EdgeInsets.all(16.0), 22 | child: Row( 23 | children: [ 24 | Expanded( 25 | child: Padding( 26 | padding: const EdgeInsets.symmetric(horizontal: 4.0), 27 | child: Column( 28 | crossAxisAlignment: CrossAxisAlignment.start, 29 | children: [ 30 | Text( 31 | "Latest Release".toUpperCase(), 32 | style: TextStyle( 33 | color: Theme.of(context).colorScheme.secondary.withOpacity(.65), 34 | fontWeight: FontWeight.w600, 35 | fontSize: 13.0, 36 | ), 37 | ), 38 | Padding( 39 | padding: const EdgeInsets.only(top: 4.0), 40 | child: Text( 41 | album.name, 42 | overflow: TextOverflow.ellipsis, 43 | style: const TextStyle( 44 | fontWeight: FontWeight.w500, 45 | ), 46 | ), 47 | ), 48 | Text( 49 | "${album.releaseDate.year} • ${album.albumType.shortTitle}", 50 | style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), 51 | ), 52 | ], 53 | ), 54 | ), 55 | ), 56 | SizedBox( 57 | width: 64, 58 | height: 64, 59 | child: CachedImage(album.images!), 60 | ), 61 | ], 62 | ), 63 | ), 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/ui/common/image_color.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:image/image.dart' as image_lib; 5 | 6 | const String keyPalette = 'palette'; 7 | const String keyNoOfItems = 'noIfItems'; 8 | 9 | int noOfPixelsPerAxis = 12; 10 | 11 | Color getAverageColor(List colors) { 12 | int r = 0, g = 0, b = 0; 13 | 14 | for (int i = 0; i < colors.length; i++) { 15 | r += colors[i].red; 16 | g += colors[i].green; 17 | b += colors[i].blue; 18 | } 19 | 20 | r = r ~/ colors.length; 21 | g = g ~/ colors.length; 22 | b = b ~/ colors.length; 23 | 24 | return Color.fromRGBO(r, g, b, 1); 25 | } 26 | 27 | Color abgrToColor(int argbColor) { 28 | int r = (argbColor >> 16) & 0xFF; 29 | int b = argbColor & 0xFF; 30 | int hex = (argbColor & 0xFF00FF00) | (b << 16) | r; 31 | return Color(hex); 32 | } 33 | 34 | List sortColors(List colors) { 35 | List sorted = []; 36 | 37 | sorted.addAll(colors); 38 | sorted.sort((a, b) => b.computeLuminance().compareTo(a.computeLuminance())); 39 | 40 | return sorted; 41 | } 42 | 43 | List generatePalette(Map params) { 44 | List colors = []; 45 | List palette = []; 46 | 47 | colors.addAll(sortColors(params[keyPalette])); 48 | 49 | int noOfItems = params[keyNoOfItems]; 50 | 51 | if (noOfItems <= colors.length) { 52 | int chunkSize = colors.length ~/ noOfItems; 53 | 54 | for (int i = 0; i < noOfItems; i++) { 55 | palette.add(getAverageColor(colors.sublist(i * chunkSize, (i + 1) * chunkSize))); 56 | } 57 | } 58 | 59 | return palette; 60 | } 61 | 62 | List extractPixelsColors(Uint8List bytes) { 63 | List colors = []; 64 | 65 | List values = bytes.buffer.asUint8List(); 66 | image_lib.Image? image = image_lib.decodeImage(values); 67 | 68 | List pixels = []; 69 | 70 | int? width = image?.width; 71 | int? height = image?.height; 72 | 73 | int xChunk = width! ~/ (noOfPixelsPerAxis + 1); 74 | int yChunk = height! ~/ (noOfPixelsPerAxis + 1); 75 | 76 | for (int j = 1; j < noOfPixelsPerAxis + 1; j++) { 77 | for (int i = 1; i < noOfPixelsPerAxis + 1; i++) { 78 | int? pixel = image?.getPixel(xChunk * i, yChunk * j); 79 | pixels.add(pixel); 80 | colors.add(abgrToColor(pixel!)); 81 | } 82 | } 83 | 84 | return colors; 85 | } 86 | 87 | List generateColorPalette(Uint8List bytes) { 88 | final samples = extractPixelsColors(bytes); 89 | final sorted = sortColors(samples); 90 | final palette = generatePalette({keyPalette: sorted, keyNoOfItems: 3}); 91 | return palette; 92 | } 93 | -------------------------------------------------------------------------------- /lib/ui/mobile/pages/library/track_loading_tile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:shimmer/shimmer.dart'; 5 | 6 | class TrackLoadingTile extends StatelessWidget { 7 | const TrackLoadingTile({Key? key, this.itemCount = 3}) : super(key: key); 8 | 9 | final int itemCount; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Padding( 14 | padding: const EdgeInsets.only(left: 16.0, right: 24.0, bottom: 8.0, top: 2.0), 15 | child: Shimmer.fromColors( 16 | baseColor: Colors.white.withOpacity(.05), 17 | highlightColor: Colors.white.withOpacity(.25), 18 | child: ListView.builder( 19 | padding: EdgeInsets.zero, 20 | shrinkWrap: true, 21 | itemCount: itemCount, 22 | physics: const NeverScrollableScrollPhysics(), 23 | itemBuilder: (_, __) => Padding( 24 | padding: const EdgeInsets.only(top: 12.0, bottom: 12.0), 25 | child: Row( 26 | crossAxisAlignment: CrossAxisAlignment.start, 27 | children: [ 28 | Container( 29 | width: 42.0, 30 | height: 42.0, 31 | decoration: BoxDecoration( 32 | color: Colors.white, 33 | borderRadius: BorderRadius.circular(8.0), 34 | ), 35 | ), 36 | const Padding( 37 | padding: EdgeInsets.symmetric(horizontal: 6.0), 38 | ), 39 | Column( 40 | crossAxisAlignment: CrossAxisAlignment.start, 41 | children: [ 42 | Container( 43 | width: Random().nextInt(125) + 100, 44 | height: 18.0, 45 | decoration: BoxDecoration( 46 | color: Colors.white, 47 | borderRadius: BorderRadius.circular(8.0), 48 | ), 49 | ), 50 | const Padding( 51 | padding: EdgeInsets.symmetric(vertical: 2.5), 52 | ), 53 | Container( 54 | width: Random().nextInt(75) + 75, 55 | height: 18.0, 56 | decoration: BoxDecoration( 57 | color: Colors.white, 58 | borderRadius: BorderRadius.circular(8.0), 59 | ), 60 | ), 61 | ], 62 | ) 63 | ], 64 | ), 65 | ), 66 | ), 67 | ), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/api/base_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:http/http.dart' as http; 4 | 5 | import 'dart:convert'; 6 | 7 | class BaseApi { 8 | static const url = "https://api.tear.one/api"; 9 | 10 | String? _accessToken; 11 | String? _refreshToken; 12 | 13 | late Function(String, String)? refreshCallback; 14 | 15 | void setAuth(String? accessToken, String? refreshToken) { 16 | log("Tokens: $accessToken, $refreshToken"); 17 | _accessToken = accessToken; 18 | _refreshToken = refreshToken; 19 | } 20 | 21 | void destroyToken() async { 22 | http.get(Uri.parse("$url/auth/destroy?refresh_token=${Uri.encodeComponent(_refreshToken!)}")); 23 | } 24 | 25 | Future getToken({int tries = 0}) async { 26 | if (_accessToken == null || tries > 3) return ""; 27 | 28 | final claims = Jwt.parseJwt(_accessToken!); 29 | 30 | // Jwt expired, generate new one 31 | if (claims["iat"] + 1800 <= (DateTime.now().millisecondsSinceEpoch / 1000).floor()) { 32 | log("Refreshing token..."); 33 | final res = await http.get(Uri.parse("$url/auth/refresh?refresh_token=${Uri.encodeComponent(_refreshToken!)}")); 34 | 35 | if (res.statusCode != 200) { 36 | log("Failed to refresh token (${res.statusCode})"); 37 | await Future.delayed(const Duration(milliseconds: 200)); 38 | return await getToken(tries: tries + 1); 39 | } 40 | 41 | log("Token refreshed"); 42 | 43 | final token = jsonDecode(res.body); 44 | _accessToken = token["access_token"]; 45 | _refreshToken = token["refresh_token"]; 46 | 47 | refreshCallback != null ? refreshCallback!(_accessToken ?? "", _refreshToken ?? "") : null; 48 | } 49 | 50 | return _accessToken!; 51 | } 52 | } 53 | 54 | class Jwt { 55 | static Map parseJwt(String token) { 56 | final parts = token.split('.'); 57 | if (parts.length != 3) { 58 | throw const FormatException('Invalid token.'); 59 | } 60 | 61 | final payload = _decodeBase64(parts[1]); 62 | final payloadMap = json.decode(payload); 63 | if (payloadMap is! Map) { 64 | throw const FormatException('Invalid payload.'); 65 | } 66 | 67 | return payloadMap; 68 | } 69 | 70 | static String _decodeBase64(String str) { 71 | String output = str.replaceAll('-', '+').replaceAll('_', '/'); 72 | 73 | switch (output.length % 4) { 74 | case 0: 75 | break; 76 | case 2: 77 | output += "=="; 78 | break; 79 | case 3: 80 | output += '='; 81 | break; 82 | default: 83 | throw Exception('Illegal base64 string.'); 84 | } 85 | 86 | return utf8.decode(base64Url.decode(output)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/firebase_options.dart: -------------------------------------------------------------------------------- 1 | // File generated by FlutterFire CLI. 2 | // ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members 3 | import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; 4 | import 'package:flutter/foundation.dart' 5 | show defaultTargetPlatform, kIsWeb, TargetPlatform; 6 | 7 | /// Default [FirebaseOptions] for use with your Firebase apps. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// import 'firebase_options.dart'; 12 | /// // ... 13 | /// await Firebase.initializeApp( 14 | /// options: DefaultFirebaseOptions.currentPlatform, 15 | /// ); 16 | /// ``` 17 | class DefaultFirebaseOptions { 18 | static FirebaseOptions get currentPlatform { 19 | if (kIsWeb) { 20 | throw UnsupportedError( 21 | 'DefaultFirebaseOptions have not been configured for web - ' 22 | 'you can reconfigure this by running the FlutterFire CLI again.', 23 | ); 24 | } 25 | switch (defaultTargetPlatform) { 26 | case TargetPlatform.android: 27 | return android; 28 | case TargetPlatform.iOS: 29 | return ios; 30 | case TargetPlatform.macOS: 31 | throw UnsupportedError( 32 | 'DefaultFirebaseOptions have not been configured for macos - ' 33 | 'you can reconfigure this by running the FlutterFire CLI again.', 34 | ); 35 | case TargetPlatform.windows: 36 | throw UnsupportedError( 37 | 'DefaultFirebaseOptions have not been configured for windows - ' 38 | 'you can reconfigure this by running the FlutterFire CLI again.', 39 | ); 40 | case TargetPlatform.linux: 41 | throw UnsupportedError( 42 | 'DefaultFirebaseOptions have not been configured for linux - ' 43 | 'you can reconfigure this by running the FlutterFire CLI again.', 44 | ); 45 | default: 46 | throw UnsupportedError( 47 | 'DefaultFirebaseOptions are not supported for this platform.', 48 | ); 49 | } 50 | } 51 | 52 | static const FirebaseOptions android = FirebaseOptions( 53 | apiKey: 'AIzaSyABQJo8Wt9mtfqSh1a2k1LuB0y1w7FhxCw', 54 | appId: '1:860150371329:android:00ba04b1deea5770651c00', 55 | messagingSenderId: '860150371329', 56 | projectId: 'tear-music', 57 | storageBucket: 'tear-music.appspot.com', 58 | ); 59 | 60 | static const FirebaseOptions ios = FirebaseOptions( 61 | apiKey: 'AIzaSyCI70uiRvIhbBeKgZA6h6eIuxRAqFyeCdI', 62 | appId: '1:860150371329:ios:9430cbf0de56c4ec651c00', 63 | messagingSenderId: '860150371329', 64 | projectId: 'tear-music', 65 | storageBucket: 'tear-music.appspot.com', 66 | iosClientId: '860150371329-esqpv6qt4g9lvgiv2l5pk8a0j5a14mav.apps.googleusercontent.com', 67 | iosBundleId: 'one.tear.tearmusic', 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /lib/models/music/lyrics.dart: -------------------------------------------------------------------------------- 1 | import 'package:tearmusic/models/model.dart'; 2 | 3 | enum LyricsType { unavailable, fullText, subtitle, richsync } 4 | 5 | typedef Subtitle = List; 6 | typedef RichSync = List; 7 | 8 | class MusicLyrics extends Model { 9 | final LyricsType lyricsType; 10 | final String? fullText; 11 | final Subtitle? subtitle; 12 | final RichSync? richSync; 13 | 14 | MusicLyrics({ 15 | required String id, 16 | required Map json, 17 | required this.lyricsType, 18 | required this.fullText, 19 | required this.richSync, 20 | required this.subtitle, 21 | }) : super(id: id, json: json, type: "lyrics"); 22 | 23 | factory MusicLyrics.decode(Map json) { 24 | Subtitle? subtitle; 25 | RichSync? richSync; 26 | 27 | if (json["subtitle"] != null) { 28 | subtitle = TimedSegment.decodeList((json["subtitle"] as List).cast()); 29 | } 30 | 31 | if (json["richsync"] != null) { 32 | richSync = LyricsLine.decodeList((json["richsync"] as List).cast()); 33 | } 34 | 35 | return MusicLyrics( 36 | json: json, 37 | id: json["id"], 38 | lyricsType: json["richsync"] != null 39 | ? LyricsType.richsync 40 | : json["subtitle"] != null 41 | ? LyricsType.subtitle 42 | : json["full_text"] != null && json["full_text"] != '' 43 | ? LyricsType.fullText 44 | : LyricsType.unavailable, 45 | fullText: json["full_text"], 46 | subtitle: subtitle, 47 | richSync: richSync, 48 | ); 49 | } 50 | 51 | Map encode() => json ?? {}; 52 | } 53 | 54 | class TimedSegment { 55 | final String text; 56 | final Duration offset; 57 | 58 | TimedSegment({required this.text, required this.offset}); 59 | 60 | factory TimedSegment.decode(Map json) { 61 | return TimedSegment( 62 | text: json['c'] ?? "", 63 | offset: Duration(milliseconds: ((json['o'] ?? 0).toDouble() * 1000).round()), 64 | ); 65 | } 66 | 67 | static List decodeList(List encoded) => encoded.map((e) => TimedSegment.decode(e)).toList().cast(); 68 | } 69 | 70 | class LyricsLine { 71 | final Duration start; 72 | final Duration end; 73 | final Subtitle segments; 74 | 75 | LyricsLine({required this.start, required this.end, required this.segments}); 76 | 77 | factory LyricsLine.decode(Map json) { 78 | return LyricsLine( 79 | start: Duration(milliseconds: ((json['ts'] ?? 0).toDouble() * 1000).round()), 80 | end: Duration(milliseconds: ((json['te'] ?? 0).toDouble() * 1000).round()), 81 | segments: TimedSegment.decodeList((json['l'] as List).cast()), 82 | ); 83 | } 84 | 85 | static List decodeList(List encoded) => encoded.map((e) => LyricsLine.decode(e)).toList().cast(); 86 | } 87 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/animated_launch_icon.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 14 | 20 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 43 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/animated_launch_icon.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 14 | 20 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 43 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /lib/models/batch.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: non_constant_identifier_names, constant_identifier_names 2 | 3 | import 'package:tearmusic/models/model.dart'; 4 | import 'package:tearmusic/models/music/album.dart'; 5 | import 'package:tearmusic/models/music/artist.dart'; 6 | import 'package:tearmusic/models/music/playlist.dart'; 7 | import 'package:tearmusic/models/music/track.dart'; 8 | 9 | class BatchLibrary { 10 | List tracks = []; 11 | List artists = []; 12 | List albums = []; 13 | List playlists = []; 14 | List track_history = []; 15 | 16 | BatchLibrary({ 17 | required this.tracks, 18 | required this.artists, 19 | required this.albums, 20 | required this.playlists, 21 | required this.track_history, 22 | }); 23 | 24 | factory BatchLibrary.decode(Map json) { 25 | return BatchLibrary( 26 | tracks: ((json["tracks"] ?? []) as List).map((e) => MusicTrack.decode(e)).toList(), 27 | artists: ((json["artists"] ?? []) as List).map((e) => MusicArtist.decode(e)).toList(), 28 | albums: ((json["albums"] ?? []) as List).map((e) => MusicAlbum.decode(e)).toList(), 29 | playlists: ((json["playlists"] ?? []) as List).map((e) => MusicPlaylist.decode(e)).toList(), 30 | track_history: ((json["track_history"] ?? []) as List).map((e) => BatchTrackHistory.decode(e)).toList()); 31 | } 32 | 33 | Map encode() => { 34 | "tracks": tracks.map((e) => e.encode()), 35 | "artists": artists.map((e) => e.encode()), 36 | "albums": albums.map((e) => e.encode()), 37 | "playlists": playlists.map((e) => e.encode()), 38 | "track_history": track_history.map((e) => e.encode()), 39 | }; 40 | } 41 | 42 | class BatchTrackHistory extends Model { 43 | MusicTrack track; 44 | Model? from; 45 | int date; 46 | 47 | BatchTrackHistory({ 48 | required this.track, 49 | this.from, 50 | required String type, 51 | required this.date, 52 | }) : super(id: track.id, type: type); 53 | 54 | factory BatchTrackHistory.decode(Map json) { 55 | return BatchTrackHistory( 56 | track: MusicTrack.decode(json["track"]), 57 | from: json["from"] != null 58 | ? (json["type"] == "playlist" 59 | ? MusicPlaylist.decode(json["from"]) 60 | : json["type"] == "album" 61 | ? MusicAlbum.decode(json["from"]) 62 | : null) 63 | : null, 64 | type: json["type"], 65 | date: json["date"], 66 | ); 67 | } 68 | 69 | Map encode() => { 70 | "track": track.encode(), 71 | "from": from != null 72 | ? type == "playlist" 73 | ? (from as MusicPlaylist).encode() 74 | : type == "album" 75 | ? (from as MusicAlbum).encode() 76 | : null 77 | : null, 78 | "type": type, 79 | "date": date, 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:hive_flutter/hive_flutter.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:firebase_core/firebase_core.dart'; 6 | import 'firebase_options.dart'; 7 | import 'package:tearmusic/api/base_api.dart'; 8 | import 'package:tearmusic/providers/current_music_provider.dart'; 9 | import 'package:tearmusic/providers/music_info_provider.dart'; 10 | import 'package:tearmusic/providers/navigator_provider.dart'; 11 | import 'package:tearmusic/providers/theme_provider.dart'; 12 | import 'package:tearmusic/providers/user_provider.dart'; 13 | import 'package:tearmusic/providers/will_pop_provider.dart'; 14 | import 'package:tearmusic/ui/mobile/app.dart'; 15 | 16 | void main() async { 17 | if (kIsWeb) return; 18 | 19 | // Initialize Hive 20 | await Hive.initFlutter(); 21 | 22 | // Initialize Firebase 23 | await Firebase.initializeApp( 24 | options: DefaultFirebaseOptions.currentPlatform, 25 | ); 26 | 27 | // Create Providers 28 | final baseApi = BaseApi(); 29 | final musicInfoProvider = MusicInfoProvider(base: baseApi); 30 | 31 | final userProvider = UserProvider(base: baseApi, musicInfo: musicInfoProvider); 32 | final currentMusicProvider = CurrentMusicProvider(); 33 | userProvider.setCurrentMusicProvider(currentMusicProvider); 34 | 35 | final themeProvider = ThemeProvider(); 36 | 37 | // Initialize background audio 38 | // await AudioService.init( 39 | // builder: () => currentMusicProvider, 40 | // config: AudioServiceConfig( 41 | // androidNotificationIcon: "mipmap/ic_splash", 42 | // androidNotificationChannelId: "one.tear.tearmusic.channel.audio", 43 | // androidNotificationChannelName: "Music playback", 44 | // androidNotificationChannelDescription: "Music playback", 45 | // androidNotificationClickStartsActivity: true, 46 | // androidNotificationOngoing: false, 47 | // androidResumeOnClick: true, 48 | // androidShowNotificationBadge: false, 49 | // androidStopForegroundOnPause: false, 50 | // fastForwardInterval: const Duration(seconds: 10), 51 | // rewindInterval: const Duration(seconds: 10), 52 | // preloadArtwork: true, 53 | // notificationColor: Colors.blue.shade900, 54 | // ), 55 | // ); 56 | 57 | // Initialize providers 58 | await userProvider.init(); 59 | await musicInfoProvider.init(); 60 | await currentMusicProvider.init(); 61 | 62 | final providers = [ 63 | ChangeNotifierProvider(create: (_) => userProvider), 64 | Provider(create: (_) => musicInfoProvider), 65 | ChangeNotifierProvider(create: (_) => themeProvider), 66 | ChangeNotifierProvider(create: (_) => currentMusicProvider), 67 | Provider(create: (_) => WillPopProvider()), 68 | ChangeNotifierProvider(create: (_) => NavigatorProvider(theme: themeProvider)), 69 | ]; 70 | 71 | // Run app 72 | runApp(App(providers: providers)); 73 | } 74 | -------------------------------------------------------------------------------- /lib/models/music/album.dart: -------------------------------------------------------------------------------- 1 | import 'package:tearmusic/models/music/artist.dart'; 2 | import 'package:tearmusic/models/music/images.dart'; 3 | import 'package:tearmusic/models/model.dart'; 4 | 5 | enum AlbumType { single, album, compilation } 6 | 7 | extension AlbumTypeTitle on AlbumType { 8 | String get shortTitle { 9 | switch (this) { 10 | case AlbumType.album: 11 | return "Album"; 12 | case AlbumType.single: 13 | return "Single"; 14 | case AlbumType.compilation: 15 | return "Album"; 16 | } 17 | } 18 | 19 | String get title { 20 | switch (this) { 21 | case AlbumType.album: 22 | return "Album"; 23 | case AlbumType.single: 24 | return "Single"; 25 | case AlbumType.compilation: 26 | return "Compilation"; 27 | } 28 | } 29 | } 30 | 31 | class MusicAlbum extends Model { 32 | final String name; 33 | final AlbumType albumType; 34 | final int trackCount; 35 | final DateTime releaseDate; 36 | final List artists; 37 | final Images? images; 38 | 39 | MusicAlbum({ 40 | required Map json, 41 | required String id, 42 | required this.name, 43 | required this.albumType, 44 | required this.trackCount, 45 | required this.releaseDate, 46 | required this.artists, 47 | required this.images, 48 | }) : super(id: id, json: json, key: "$name ${artists.first.name}", type: "album"); 49 | 50 | factory MusicAlbum.decode(Map json) { 51 | final images = json["images"] as List?; 52 | return MusicAlbum( 53 | json: json, 54 | id: json["id"], 55 | name: json["name"], 56 | albumType: AlbumType.values[["single", "album", "compilation"].indexOf(json["album_type"] ?? "album")], 57 | trackCount: json["track_count"] ?? 0, 58 | releaseDate: DateTime.tryParse(json["release_date"] ?? "") ?? DateTime.fromMillisecondsSinceEpoch(0), 59 | artists: (json["artists"] as List).map((e) => MusicArtist.decode(e)).toList().cast(), 60 | images: images != null && images.isNotEmpty ? Images.decode(images.cast()) : null, 61 | ); 62 | } 63 | 64 | Map encode() => json ?? {}; 65 | 66 | static List decodeList(List encoded) => encoded.where((e) => e["id"] != null).map((e) => MusicAlbum.decode(e)).toList().cast(); 67 | static List encodeList(List models) => models.map((e) => e.encode()).toList().cast(); 68 | 69 | String get artistsLabel { 70 | if (artists.length == 2) { 71 | return "${artists[0].name} & ${artists[1].name}"; 72 | } 73 | return artists.map((e) => e.name).join(", "); 74 | } 75 | 76 | String get shortTitle { 77 | if (albumType == AlbumType.single && trackCount > 1) { 78 | return "EP"; 79 | } 80 | return albumType.shortTitle; 81 | } 82 | 83 | String get title { 84 | if (albumType == AlbumType.single && trackCount > 1) { 85 | return "Extended Play"; 86 | } 87 | return albumType.title; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /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 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/player/track_image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:animations/animations.dart'; 4 | import 'package:figma_squircle/figma_squircle.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:tearmusic/ui/mobile/common/cached_image.dart'; 7 | import 'package:tearmusic/utils.dart'; 8 | 9 | class TrackImage extends StatelessWidget { 10 | const TrackImage({ 11 | Key? key, 12 | required this.image, 13 | required this.bottomOffset, 14 | required this.maxOffset, 15 | required this.screenSize, 16 | required this.cp, 17 | required this.p, 18 | this.width = 82.0, 19 | this.bytes, 20 | this.large = false, 21 | }) : super(key: key); 22 | 23 | final CachedImage image; 24 | final bool large; 25 | 26 | final double width; 27 | 28 | final double bottomOffset; 29 | final double maxOffset; 30 | final Size screenSize; 31 | final double cp; 32 | final double p; 33 | final Uint8List? bytes; 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | final radius = rangeProgress(a: 14.0, b: 32.0, c: cp); 38 | final borderRadius = SmoothBorderRadius(cornerRadius: radius, cornerSmoothing: 1.0); 39 | final size = rangeProgress(a: width, b: screenSize.width - 84.0, c: cp); 40 | // const imgSize = Size(400, 400); 41 | 42 | return Transform.translate( 43 | offset: Offset(0, bottomOffset + (-maxOffset / 2.15 * p.clamp(0, 2))), 44 | child: Padding( 45 | padding: EdgeInsets.all(12.0 * (1 - cp)).add(EdgeInsets.only(left: 42.0 * cp)), 46 | child: Align( 47 | alignment: Alignment.bottomLeft, 48 | child: SizedBox( 49 | height: size, 50 | width: size, 51 | child: Padding( 52 | padding: EdgeInsets.all(12.0 * (1 - cp)), 53 | child: PageTransitionSwitcher( 54 | transitionBuilder: (child, primaryAnimation, secondaryAnimation) { 55 | return FadeThroughTransition( 56 | fillColor: Colors.transparent, 57 | animation: primaryAnimation, 58 | secondaryAnimation: secondaryAnimation, 59 | child: child, 60 | ); 61 | }, 62 | child: Container( 63 | key: const Key("imgcontainer"), 64 | decoration: ShapeDecoration( 65 | shape: SmoothRectangleBorder(borderRadius: borderRadius), 66 | shadows: [ 67 | BoxShadow( 68 | color: Colors.black.withOpacity(.25 * cp), 69 | blurRadius: 24.0, 70 | offset: const Offset(0.0, 4.0), 71 | ), 72 | ], 73 | ), 74 | child: ClipRRect(borderRadius: borderRadius, child: image), 75 | ), 76 | ), 77 | ), 78 | ), 79 | ), 80 | ), 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/providers/navigator_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; 3 | import 'package:tearmusic/providers/theme_provider.dart'; 4 | import 'package:tearmusic/ui/mobile/common/bottom_sheet.dart'; 5 | import 'package:tearmusic/ui/mobile/navigator.dart'; 6 | 7 | class NavigatorPageState { 8 | final List _uriHistory = []; 9 | NavigatorState _state; 10 | 11 | NavigatorPageState({ 12 | required NavigatorState state, 13 | }) : _state = state; 14 | } 15 | 16 | class NavigatorProvider extends ChangeNotifier { 17 | late ScaffoldMessengerState _messenger; 18 | final ThemeProvider _theme; 19 | 20 | MobileRoute? currentRoute; 21 | final Map _pages = {}; 22 | NavigatorPageState get _currentPage => _pages[currentRoute]!; 23 | 24 | List get _uriHistory => _currentPage._uriHistory; 25 | NavigatorState get _state => _currentPage._state; 26 | 27 | NavigatorProvider({required ThemeProvider theme}) : _theme = theme; 28 | 29 | void setScaffoldState(ScaffoldMessengerState value) { 30 | _messenger = value; 31 | } 32 | 33 | void setState(MobileRoute route, NavigatorState value) { 34 | if (!_pages.keys.contains(route)) { 35 | _pages[route] = NavigatorPageState(state: value); 36 | } else { 37 | _pages[route]!._state = value; 38 | } 39 | } 40 | 41 | void restoreState(MobileRoute route, {bool notify = false}) { 42 | currentRoute = route; 43 | if (notify) notifyListeners(); 44 | } 45 | 46 | Future push(Route route, {String? uri}) { 47 | if (uri != null) { 48 | if (_uriHistory.isNotEmpty && _uriHistory.last == uri) return Future.value(null); 49 | _uriHistory.add(uri); 50 | } 51 | return _state.push(route); 52 | } 53 | 54 | static const Radius _kDefaultTopRadius = Radius.circular(12); 55 | 56 | Future pushModal({required Widget Function(BuildContext) builder, String? uri}) { 57 | return push( 58 | CupertinoModalBottomSheetRoute( 59 | builder: builder, 60 | containerBuilder: (context, _, child) => BottomSheetContainer( 61 | topRadius: _kDefaultTopRadius, 62 | child: child, 63 | ), 64 | expanded: false, 65 | duration: const Duration(milliseconds: 300), 66 | animationCurve: Curves.fastLinearToSlowEaseIn, 67 | ), 68 | uri: uri, 69 | ).then((value) { 70 | if (_uriHistory.isNotEmpty) _uriHistory.removeLast(); 71 | return value; 72 | }); 73 | } 74 | 75 | void pop([T? result]) { 76 | if (_uriHistory.isNotEmpty) _uriHistory.removeLast(); 77 | _state.pop(result); 78 | } 79 | 80 | void clearHistory() { 81 | _uriHistory.clear(); 82 | } 83 | 84 | void showSnackBar(SnackBar snackBar) { 85 | final theme = _theme.appTheme; 86 | _messenger.showSnackBar(SnackBar( 87 | content: DefaultTextStyle( 88 | style: theme.textTheme.bodyMedium!, 89 | child: snackBar.content, 90 | ), 91 | backgroundColor: theme.colorScheme.background, 92 | )); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/ui/mobile/pages/library/playlist_loading_tile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:shimmer/shimmer.dart'; 5 | 6 | class PlaylistLoadingTile extends StatelessWidget { 7 | const PlaylistLoadingTile({Key? key, this.itemCount = 3}) : super(key: key); 8 | 9 | final int itemCount; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Padding( 14 | padding: const EdgeInsets.only(left: 16.0, right: 24.0, bottom: 8.0, top: 2.0), 15 | child: Shimmer.fromColors( 16 | baseColor: Colors.white.withOpacity(.05), 17 | highlightColor: Colors.white.withOpacity(.25), 18 | child: ListView.builder( 19 | padding: EdgeInsets.zero, 20 | shrinkWrap: true, 21 | itemCount: itemCount, 22 | itemBuilder: (_, __) => Padding( 23 | padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), 24 | child: Row( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | Container( 28 | width: 42.0, 29 | height: 42.0, 30 | decoration: BoxDecoration( 31 | color: Colors.white, 32 | borderRadius: BorderRadius.circular(8.0), 33 | ), 34 | ), 35 | const Padding( 36 | padding: EdgeInsets.symmetric(horizontal: 6.0), 37 | ), 38 | Column( 39 | crossAxisAlignment: CrossAxisAlignment.start, 40 | children: [ 41 | Container( 42 | width: Random().nextInt(125) + 100, 43 | height: 18.0, 44 | decoration: BoxDecoration( 45 | color: Colors.white, 46 | borderRadius: BorderRadius.circular(8.0), 47 | ), 48 | ), 49 | const Padding( 50 | padding: EdgeInsets.symmetric(vertical: 2.5), 51 | ), 52 | Row( 53 | children: [ 54 | Container( 55 | width: Random().nextInt(50) + 50, 56 | height: 18.0, 57 | decoration: BoxDecoration( 58 | color: Colors.white, 59 | borderRadius: BorderRadius.circular(8.0), 60 | ), 61 | ), 62 | const SizedBox(width: 8.0), 63 | Container( 64 | width: Random().nextInt(50) + 50, 65 | height: 18.0, 66 | decoration: BoxDecoration( 67 | color: Colors.white, 68 | borderRadius: BorderRadius.circular(8.0), 69 | ), 70 | ), 71 | ], 72 | ), 73 | ], 74 | ) 75 | ], 76 | ), 77 | ), 78 | ), 79 | ), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/models/search.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tearmusic/models/music/album.dart'; 3 | import 'package:tearmusic/models/music/artist.dart'; 4 | import 'package:tearmusic/models/music/playlist.dart'; 5 | import 'package:tearmusic/models/music/track.dart'; 6 | 7 | class SearchResults { 8 | final List tracks; 9 | final List playlists; 10 | final List albums; 11 | final List artists; 12 | 13 | SearchResults({required this.tracks, required this.playlists, required this.albums, required this.artists}); 14 | 15 | factory SearchResults.decode(Map json) { 16 | return SearchResults( 17 | tracks: MusicTrack.decodeList((json["tracks"] as List).cast()), 18 | playlists: MusicPlaylist.decodeList((json["playlists"] as List).cast()), 19 | albums: MusicAlbum.decodeList((json["albums"] as List).cast()), 20 | artists: MusicArtist.decodeList((json["artists"] as List).cast()), 21 | ); 22 | } 23 | 24 | factory SearchResults.decodeFilter(Map json, {required String filter}) { 25 | final tracks = MusicTrack.decodeList((json["tracks"] as List).cast()); 26 | final playlists = MusicPlaylist.decodeList((json["playlists"] as List).cast()); 27 | final albums = MusicAlbum.decodeList((json["albums"] as List).cast()); 28 | final artists = MusicArtist.decodeList((json["artists"] as List).cast()); 29 | 30 | return SearchResults( 31 | tracks: tracks.where((e) => e.match(filter)).toList(), 32 | playlists: playlists.where((e) => e.match(filter)).toList(), 33 | albums: albums.where((e) => e.match(filter)).toList(), 34 | artists: artists.where((e) => e.match(filter)).toList(), 35 | ); 36 | } 37 | 38 | bool get isEmpty => tracks.isEmpty && playlists.isEmpty && albums.isEmpty && artists.isEmpty; 39 | bool get isNotEmpty => !isEmpty; 40 | } 41 | 42 | class SearchSuggestionPart { 43 | final String text; 44 | final bool bold; 45 | 46 | SearchSuggestionPart({ 47 | required this.text, 48 | required this.bold, 49 | }); 50 | } 51 | 52 | class SearchSuggestion { 53 | final List _parts; 54 | 55 | SearchSuggestion({ 56 | required List parts, 57 | }) : _parts = parts; 58 | 59 | factory SearchSuggestion.decode(List json) { 60 | return SearchSuggestion( 61 | parts: json 62 | .map((e) => SearchSuggestionPart( 63 | text: e['text'], 64 | bold: e['bold'] ?? false, 65 | )) 66 | .toList(), 67 | ); 68 | } 69 | 70 | static List decodeList(List encoded) => encoded.map((e) => SearchSuggestion.decode(e)).toList().cast(); 71 | 72 | String get raw => _parts.map((e) => e.text).join(); 73 | 74 | List render(BuildContext context) { 75 | return _parts 76 | .map((e) => TextSpan( 77 | text: e.text, 78 | style: TextStyle( 79 | fontWeight: e.bold ? FontWeight.bold : FontWeight.w500, 80 | color: e.bold ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.secondary, 81 | ), 82 | )) 83 | .toList(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /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 | // START: FlutterFire Configuration 26 | apply plugin: 'com.google.gms.google-services' 27 | // END: FlutterFire Configuration 28 | apply plugin: 'kotlin-android' 29 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 30 | 31 | def homePath = System.properties['user.home'] 32 | def keystoreProperties = new Properties() 33 | def keystorePropertiesFile = rootProject.file(homePath + '/keys/tearone.properties') 34 | if (keystorePropertiesFile.exists()) { 35 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 36 | } 37 | 38 | android { 39 | compileSdkVersion flutter.compileSdkVersion 40 | ndkVersion flutter.ndkVersion 41 | 42 | compileOptions { 43 | sourceCompatibility JavaVersion.VERSION_1_8 44 | targetCompatibility JavaVersion.VERSION_1_8 45 | } 46 | 47 | kotlinOptions { 48 | jvmTarget = '1.8' 49 | } 50 | 51 | sourceSets { 52 | main.java.srcDirs += 'src/main/kotlin' 53 | } 54 | 55 | defaultConfig { 56 | applicationId "one.tear.tearmusic" 57 | minSdkVersion flutter.minSdkVersion 58 | targetSdkVersion flutter.targetSdkVersion 59 | versionCode flutterVersionCode.toInteger() 60 | versionName flutterVersionName 61 | } 62 | 63 | signingConfigs { 64 | release { 65 | if (System.getenv()["CI"]) { // CI=true is exported by Codemagic 66 | keyAlias System.getenv()["CM_KEY_ALIAS"] 67 | keyPassword System.getenv()["CM_KEY_PASSWORD"] 68 | storePassword System.getenv()["CM_KEYSTORE_PASSWORD"] 69 | storeFile file(System.getenv()["CM_KEYSTORE_PATH"]) 70 | } else { 71 | keyAlias keystoreProperties['keyAlias'] 72 | keyPassword keystoreProperties['keyPassword'] 73 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 74 | storePassword keystoreProperties['storePassword'] 75 | } 76 | } 77 | } 78 | 79 | buildTypes { 80 | release { 81 | if (keystorePropertiesFile.exists()) { 82 | signingConfig signingConfigs.release 83 | println "Signing with key.properties" 84 | } else { 85 | signingConfig signingConfigs.debug 86 | println "Signing with debug keys" 87 | } 88 | } 89 | } 90 | } 91 | 92 | flutter { 93 | source '../..' 94 | } 95 | 96 | dependencies { 97 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 98 | } 99 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/player/lyrics_view/subtitle.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:tearmusic/models/music/lyrics.dart'; 4 | import 'package:tearmusic/providers/current_music_provider.dart'; 5 | 6 | Widget Function(BuildContext, int) subtitleListBuilder(List subtitle) { 7 | return (context, index) { 8 | final currentMusic = context.read(); 9 | 10 | final subtitleLine = subtitle[index]; 11 | final subtitleNext = subtitle[(index + 1).clamp(0, subtitle.length - 1)]; 12 | final progress = subtitleLine.offset.inMilliseconds / (currentMusic.duration?.inMilliseconds ?? 1); 13 | final progressEnd = (subtitleNext.offset.inMilliseconds - 200) / (currentMusic.duration?.inMilliseconds ?? 1); 14 | 15 | // if (actives[index][0] != (animation.value > progress)) { 16 | // actives[index][0] = animation.value > progress; 17 | // if (subtitle.text.replaceAll(" ", "") != "") HapticFeedback.lightImpact(); 18 | // } 19 | 20 | String text = subtitleLine.text; 21 | if (text.trim() == "") { 22 | text = "🎶"; 23 | } 24 | 25 | return StreamBuilder( 26 | stream: currentMusic.positionStream, 27 | builder: (context, snapshot) { 28 | final value = snapshot.hasData && currentMusic.duration != null 29 | ? snapshot.data!.inMilliseconds / (currentMusic.duration?.inMilliseconds ?? 1) 30 | : 0.0; 31 | return Padding( 32 | padding: const EdgeInsets.symmetric(horizontal: 12.0), 33 | child: InkWell( 34 | borderRadius: BorderRadius.circular(12.0), 35 | onTap: () { 36 | currentMusic.seek(subtitleLine.offset); 37 | }, 38 | child: AnimatedContainer( 39 | duration: const Duration(milliseconds: 500), 40 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 14.0), 41 | decoration: BoxDecoration( 42 | color: value > progress && value < progressEnd ? Theme.of(context).colorScheme.secondary.withOpacity(.1) : Colors.transparent, 43 | borderRadius: BorderRadius.circular(12.0), 44 | ), 45 | child: AnimatedDefaultTextStyle( 46 | duration: const Duration(milliseconds: 200), 47 | style: TextStyle( 48 | color: value > progress 49 | ? value < progressEnd 50 | ? Theme.of(context).colorScheme.primary 51 | : Theme.of(context).colorScheme.secondary 52 | : Theme.of(context).colorScheme.secondary.withOpacity(.3), 53 | fontFamily: Theme.of(context).textTheme.bodyMedium!.fontFamily, 54 | fontWeight: FontWeight.bold, 55 | fontSize: 22.0, 56 | shadows: [ 57 | Shadow( 58 | offset: const Offset(5.0, 6.0), 59 | blurRadius: 0.0, 60 | color: Theme.of(context).colorScheme.secondary.withOpacity(value > progressEnd ? .15 : 0), 61 | ), 62 | ], 63 | ), 64 | child: Text( 65 | text, 66 | textAlign: TextAlign.center, 67 | ), 68 | ), 69 | ), 70 | ), 71 | ); 72 | }, 73 | ); 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: tearmusic 2 | description: Music Player Frontend 3 | 4 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 5 | 6 | version: 1.0.0+10 7 | 8 | environment: 9 | sdk: ">=2.18.0-271.4.beta <3.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | cupertino_icons: ^1.0.2 16 | hive: ^2.2.3 17 | hive_flutter: ^1.1.0 18 | url_launcher: ^6.1.5 19 | uni_links: ^0.5.1 20 | clipboard: ^0.1.3 21 | provider: ^6.0.3 22 | ionicons: ^0.2.1 23 | lottie: ^1.4.1 24 | http: ^0.13.5 25 | animations: ^2.0.3 26 | figma_squircle: ^0.5.3 27 | like_button: ^2.0.4 28 | loading_animation_widget: ^1.2.0+2 29 | image: ^3.2.0 30 | intl: ^0.17.0 31 | audio_session: ^0.1.10 32 | audio_service: ^0.18.7 33 | firebase_core: ^1.20.1 34 | cbor: ^5.1.0 35 | uuid: ^3.0.6 36 | pubnub: 37 | git: 38 | url: https://github.com/55nknown/pubnub 39 | ref: master 40 | path: pubnub 41 | fuzzy: ^0.4.0-nullsafety.0 42 | animated_flip_counter: ^0.2.5 43 | animated_background: ^2.0.0 44 | automatic_animated_list: ^1.1.0 45 | shimmer: ^2.0.0 46 | wakelock: ^0.6.2 47 | async: ^2.9.0 48 | modal_bottom_sheet: ^2.1.0 49 | audioplayers: ^1.2.0 50 | 51 | dev_dependencies: 52 | flutter_test: 53 | sdk: flutter 54 | 55 | flutter_lints: ^2.0.0 56 | hive_generator: ^1.1.3 57 | build_runner: ^2.2.0 58 | 59 | flutter: 60 | uses-material-design: true 61 | 62 | assets: 63 | - assets/ 64 | 65 | fonts: 66 | - family: Montserrat 67 | fonts: 68 | - asset: fonts/Montserrat/Montserrat-Black.otf 69 | weight: 900 70 | - asset: fonts/Montserrat/Montserrat-BlackItalic.otf 71 | weight: 900 72 | style: italic 73 | - asset: fonts/Montserrat/Montserrat-ExtraBold.otf 74 | weight: 800 75 | - asset: fonts/Montserrat/Montserrat-ExtraBoldItalic.otf 76 | weight: 800 77 | style: italic 78 | - asset: fonts/Montserrat/Montserrat-Bold.otf 79 | weight: 700 80 | - asset: fonts/Montserrat/Montserrat-BoldItalic.otf 81 | weight: 700 82 | style: italic 83 | - asset: fonts/Montserrat/Montserrat-SemiBold.otf 84 | weight: 600 85 | - asset: fonts/Montserrat/Montserrat-SemiBoldItalic.otf 86 | weight: 600 87 | style: italic 88 | - asset: fonts/Montserrat/Montserrat-Medium.otf 89 | weight: 500 90 | - asset: fonts/Montserrat/Montserrat-MediumItalic.otf 91 | weight: 500 92 | style: italic 93 | - asset: fonts/Montserrat/Montserrat-Regular.otf 94 | weight: 400 95 | - asset: fonts/Montserrat/Montserrat-Italic.otf 96 | weight: 400 97 | style: italic 98 | - asset: fonts/Montserrat/Montserrat-Light.otf 99 | weight: 300 100 | - asset: fonts/Montserrat/Montserrat-LightItalic.otf 101 | weight: 300 102 | style: italic 103 | - asset: fonts/Montserrat/Montserrat-ExtraLight.otf 104 | weight: 200 105 | - asset: fonts/Montserrat/Montserrat-ExtraLightItalic.otf 106 | weight: 200 107 | style: italic 108 | - asset: fonts/Montserrat/Montserrat-Thin.otf 109 | weight: 100 110 | - asset: fonts/Montserrat/Montserrat-ThinItalic.otf 111 | weight: 100 112 | style: italic 113 | -------------------------------------------------------------------------------- /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/ui/mobile/common/filter_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FilterBar extends StatefulWidget implements PreferredSizeWidget { 4 | const FilterBar( 5 | {Key? key, 6 | required this.items, 7 | required this.controller, 8 | this.onTap, 9 | this.padding = const EdgeInsets.symmetric(horizontal: 24.0), 10 | this.disableFading = false, 11 | this.scrollable = true}) 12 | : assert(items.length == controller.length), 13 | super(key: key); 14 | 15 | final List items; 16 | final TabController controller; 17 | final EdgeInsetsGeometry padding; 18 | final Function(int)? onTap; 19 | @override 20 | final Size preferredSize = const Size.fromHeight(42.0); 21 | final bool disableFading; 22 | final bool scrollable; 23 | 24 | @override 25 | State createState() => _FilterBarState(); 26 | } 27 | 28 | class _FilterBarState extends State { 29 | @override 30 | void initState() { 31 | super.initState(); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | final tabbar = TabBar( 37 | controller: widget.controller, 38 | isScrollable: widget.scrollable, 39 | physics: const BouncingScrollPhysics(), 40 | // Label 41 | labelStyle: Theme.of(context).textTheme.labelMedium!.copyWith( 42 | fontWeight: FontWeight.w600, 43 | fontSize: 15.0, 44 | ), 45 | labelPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3), 46 | labelColor: Theme.of(context).colorScheme.secondary, 47 | unselectedLabelColor: Colors.white.withOpacity(0.65), 48 | // Indicator 49 | indicatorPadding: const EdgeInsets.symmetric(vertical: 8), 50 | indicator: BoxDecoration( 51 | color: Theme.of(context).colorScheme.secondary.withOpacity(0.25), 52 | borderRadius: BorderRadius.circular(45.0), 53 | ), 54 | overlayColor: MaterialStateProperty.all(const Color(0x00000000)), 55 | // Tabs 56 | padding: EdgeInsets.zero, 57 | tabs: widget.items, 58 | onTap: widget.onTap, 59 | ); 60 | 61 | return Container( 62 | width: MediaQuery.of(context).size.width, 63 | height: 48.0, 64 | padding: widget.padding, 65 | child: widget.disableFading 66 | ? tabbar 67 | : AnimatedBuilder( 68 | animation: widget.controller.animation!, 69 | builder: (ctx, child) { 70 | // avoid fading over selected tab 71 | return ShaderMask( 72 | shaderCallback: (Rect bounds) { 73 | final Color bg = Theme.of(context).scaffoldBackgroundColor; 74 | final double index = widget.controller.animation!.value; 75 | return LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ 76 | index < 0.2 ? Colors.transparent : bg, 77 | Colors.transparent, 78 | Colors.transparent, 79 | index > widget.controller.length - 1.2 ? Colors.transparent : bg 80 | ], stops: const [ 81 | 0, 82 | 0.1, 83 | 0.9, 84 | 1 85 | ]).createShader(bounds); 86 | }, 87 | blendMode: BlendMode.dstOut, 88 | child: child); 89 | }, 90 | child: tabbar)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/models/player_info.dart: -------------------------------------------------------------------------------- 1 | class PlayerInfo { 2 | QueueItem? currentMusic; 3 | QueueSource queueSource; 4 | List normalQueue; 5 | List primaryQueue; 6 | List queueHistory; 7 | List operations; 8 | int version; 9 | late int operationsVersion; // start version of the operations 10 | 11 | PlayerInfo({ 12 | List? operations, 13 | List? normalQueue, 14 | List? primaryQueue, 15 | List? queueHistory, 16 | this.operationsVersion = 0, 17 | this.currentMusic, 18 | required this.queueSource, 19 | required this.version, 20 | }) : operations = List.from(operations ?? []), 21 | normalQueue = List.from(normalQueue ?? []), 22 | primaryQueue = List.from(primaryQueue ?? []), 23 | queueHistory = List.from(queueHistory ?? []); 24 | 25 | // ik server response dont include operations field, but db save may include. 26 | factory PlayerInfo.decode(Map json) { 27 | return PlayerInfo( 28 | normalQueue: (json["normal_queue"] as List).cast(), 29 | primaryQueue: (json["primary_queue"] as List).cast(), 30 | queueHistory: (json["queue_history"] as List).map((e) => QueueItem.decode(e)).toList(), 31 | version: json["version"] ?? 0, 32 | operations: ((json["operations"] ?? []) as List).cast().toList(), 33 | operationsVersion: json["operations_version"] ?? 0, 34 | currentMusic: json["current_music"] != null ? QueueItem.decode(json["current_music"]) : null, 35 | queueSource: QueueSource.decode(json["queue_source"]), 36 | ); 37 | } 38 | 39 | Map encode() { 40 | return { 41 | "normal_queue": normalQueue, 42 | "primary_queue": primaryQueue, 43 | "queue_history": queueHistory.map((e) => e.encode()).toList(), 44 | "version": version, 45 | "operations": operations, 46 | "operations_version": operationsVersion, 47 | "current_music": currentMusic?.encode(), 48 | "queue_source": queueSource.encode(), 49 | }; 50 | } 51 | } 52 | 53 | enum PlayerInfoPostType { primary, normal, history, current } 54 | 55 | enum PlayerInfoReorderMoveType { primary, normal } 56 | 57 | enum PlayerInfoSourceType { playlist, album, artist, radio } 58 | 59 | class QueueSource { 60 | int? seed; 61 | String? id; 62 | int? index; 63 | PlayerInfoSourceType type; 64 | 65 | final types = { 66 | 'playlist': PlayerInfoSourceType.playlist, 67 | 'album': PlayerInfoSourceType.album, 68 | 'artist': PlayerInfoSourceType.artist, 69 | 'radio': PlayerInfoSourceType.radio, 70 | }; 71 | 72 | QueueSource({ 73 | this.seed, 74 | this.id, 75 | this.index, 76 | required this.type, 77 | }); 78 | 79 | factory QueueSource.decode(Map json) { 80 | return QueueSource( 81 | seed: json["seed"], 82 | id: json["id"], 83 | index: json["index"], 84 | type: PlayerInfoSourceType.values.firstWhere((element) => element.name == json["type"]), 85 | ); 86 | } 87 | 88 | Map encode() => { 89 | "seed": seed, 90 | "id": id, 91 | "index": index, 92 | "type": type.name, 93 | }; 94 | } 95 | 96 | class QueueItem { 97 | String id; 98 | bool fromPrimary; 99 | 100 | QueueItem({required this.id, required this.fromPrimary}); 101 | 102 | factory QueueItem.decode(Map json) { 103 | return QueueItem(id: json["id"], fromPrimary: json["fromPrimary"]); 104 | } 105 | 106 | Map encode() => { 107 | "id": id, 108 | "fromPrimary": fromPrimary, 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/views/content_list_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:automatic_animated_list/automatic_animated_list.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:tearmusic/models/model.dart'; 5 | import 'package:tearmusic/ui/mobile/common/wallpaper.dart'; 6 | 7 | typedef ContentListViewBuilder = Widget? Function(ValueWidgetBuilder)?; 8 | typedef ContentListViewItemBuilder = Widget? Function(BuildContext, T); 9 | typedef ContentListViewRetriever = Future>? Function(); 10 | 11 | class ContentListView extends StatefulWidget { 12 | const ContentListView({ 13 | Key? key, 14 | this.title, 15 | this.emptyTitle, 16 | required this.loadingWidget, 17 | required this.itemBuilder, 18 | required this.retriever, 19 | this.builder, 20 | }) : super(key: key); 21 | 22 | final Widget? title; 23 | final Widget? emptyTitle; 24 | final Widget loadingWidget; 25 | final ContentListViewItemBuilder itemBuilder; 26 | final ContentListViewRetriever retriever; 27 | final ContentListViewBuilder builder; 28 | 29 | @override 30 | State> createState() => _ContentListViewState(); 31 | } 32 | 33 | class _ContentListViewState extends State> { 34 | Widget itemBuilder(BuildContext context, U? value, Widget? child) { 35 | if ((value as List?)?.isEmpty ?? false) { 36 | return Padding( 37 | padding: const EdgeInsets.only(top: 6.0, bottom: 24.0), 38 | child: Center( 39 | child: widget.emptyTitle, 40 | ), 41 | ); 42 | } 43 | 44 | return FutureBuilder>( 45 | future: widget.retriever(), 46 | builder: ((context, snapshot) { 47 | if (!snapshot.hasData) { 48 | return widget.loadingWidget; 49 | } 50 | 51 | final List items = snapshot.data!; 52 | 53 | return AutomaticAnimatedList( 54 | shrinkWrap: true, 55 | physics: const NeverScrollableScrollPhysics(), 56 | items: items, 57 | keyingFunction: (item) => Key(item.id), 58 | itemBuilder: (BuildContext context, T item, Animation animation) { 59 | return FadeTransition( 60 | key: Key(item.id), 61 | opacity: animation, 62 | child: SizeTransition( 63 | sizeFactor: CurvedAnimation( 64 | parent: animation, 65 | curve: Curves.easeOut, 66 | reverseCurve: Curves.easeIn, 67 | ), 68 | child: widget.itemBuilder(context, item), 69 | ), 70 | ); 71 | }, 72 | ); 73 | }), 74 | ); 75 | } 76 | 77 | @override 78 | Widget build(BuildContext context) { 79 | return Scaffold( 80 | body: Wallpaper( 81 | gradient: false, 82 | child: CupertinoScrollbar( 83 | child: CustomScrollView( 84 | slivers: [ 85 | SliverAppBar( 86 | floating: false, 87 | pinned: true, 88 | snap: false, 89 | title: widget.title, 90 | ), 91 | SliverToBoxAdapter( 92 | child: widget.builder != null ? widget.builder!(itemBuilder) : itemBuilder(context, null, null), 93 | ), 94 | const SliverToBoxAdapter( 95 | child: SafeArea( 96 | top: false, 97 | child: SizedBox(height: 100), 98 | ), 99 | ), 100 | ], 101 | ), 102 | ), 103 | ), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/api/user_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:developer'; 3 | 4 | import 'package:tearmusic/api/base_api.dart'; 5 | import 'package:http/http.dart' as http; 6 | import 'package:tearmusic/api/music_api.dart'; 7 | import 'package:tearmusic/exceptionts.dart'; 8 | import 'package:tearmusic/models/library.dart'; 9 | import 'package:tearmusic/models/player_info.dart'; 10 | import 'package:tearmusic/models/user_info.dart'; 11 | 12 | class UserApi { 13 | UserApi({required this.base}); 14 | 15 | BaseApi base; 16 | 17 | void _reschk(http.Response res, String cause) { 18 | cause = "UserApi.$cause"; 19 | if (res.statusCode == 401) { 20 | throw AuthException(cause); 21 | } 22 | if (res.statusCode == 404) { 23 | throw NotFoundException(cause); 24 | } 25 | if (res.statusCode != 200) { 26 | log("Unknown Request: ${res.statusCode}"); 27 | throw UnknownRequestException(cause); 28 | } 29 | } 30 | 31 | Future getInfo() async { 32 | final res = await http.get( 33 | Uri.parse("${BaseApi.url}/user/info"), 34 | headers: {"authorization": await base.getToken()}, 35 | ); 36 | 37 | _reschk(res, "getInfo"); 38 | 39 | return UserInfo.decode(jsonDecode(res.body)); 40 | } 41 | 42 | Future getLibrary() async { 43 | final res = await http.get( 44 | Uri.parse("${MusicApi.baseUrl}/user/music-library"), 45 | headers: {"authorization": await base.getToken()}, 46 | ); 47 | 48 | _reschk(res, "getLibrary"); 49 | 50 | return UserLibrary.decode(jsonDecode(res.body)); 51 | } 52 | 53 | Future putLibrary(String id, LibraryType type, {String? from, String? fromType}) async { 54 | final res = await http.put(Uri.parse("${MusicApi.baseUrl}/user/music-library"), 55 | headers: {"authorization": await base.getToken(), "content-type": "application/json"}, 56 | body: jsonEncode({ 57 | "id": id, 58 | "type": type.name, 59 | "from": from, 60 | "from_type": fromType, 61 | })); 62 | 63 | _reschk(res, "putLibrary"); 64 | } 65 | 66 | Future deleteLibrary(String id, LibraryType type) async { 67 | final res = await http.delete(Uri.parse("${MusicApi.baseUrl}/user/music-library"), 68 | headers: {"authorization": await base.getToken(), "content-type": "application/json"}, body: jsonEncode({"id": id, "type": type.name})); 69 | 70 | _reschk(res, "deleteLibrary"); 71 | } 72 | 73 | // QUEUE STUFF 74 | 75 | Future getPlayerInfo() async { 76 | final res = await http.get( 77 | Uri.parse("${MusicApi.baseUrl}/user/player-info"), 78 | headers: {"authorization": await base.getToken()}, 79 | ); 80 | 81 | _reschk(res, "getPlayerInfo"); 82 | 83 | return PlayerInfo.decode(jsonDecode(res.body)); 84 | } 85 | 86 | Future getPlayerVersion() async { 87 | final res = await http.head( 88 | Uri.parse("${MusicApi.baseUrl}/user/player-info"), 89 | headers: {"authorization": await base.getToken()}, 90 | ); 91 | 92 | _reschk(res, "getPlayerVersion"); 93 | 94 | return int.tryParse(res.headers["x-tmc-version"]!) ?? 0; 95 | } 96 | 97 | Future syncPlayerOperations(PlayerInfo playerInfo) async { 98 | final res = await http.post( 99 | Uri.parse("${MusicApi.baseUrl}/user/player-info?version=${playerInfo.version}&operations_version=${playerInfo.operationsVersion}"), 100 | headers: {"authorization": await base.getToken()}, 101 | body: {"operations": jsonEncode(playerInfo.operations)}, 102 | ); 103 | 104 | log("syncPlayerOperations tried to sync: ${playerInfo.operations}"); 105 | log("syncPlayerOperations code: ${res.statusCode}"); 106 | 107 | //_reschk(res, "syncPlayerOperations"); 108 | 109 | // return success or not 110 | return res.statusCode == 200; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/settings/settings_alert_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:ionicons/ionicons.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:tearmusic/providers/user_provider.dart'; 5 | 6 | class SettingsAlertDialog { 7 | void showCustomDialog(BuildContext context) { 8 | showDialog( 9 | context: context, 10 | builder: (c) => Dialog( 11 | child: Container( 12 | decoration: BoxDecoration( 13 | borderRadius: BorderRadius.circular(12.0), 14 | color: Theme.of(context).colorScheme.background, 15 | ), 16 | height: 250.0, 17 | width: 300.0, 18 | child: Column( 19 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 20 | children: [ 21 | const Padding( 22 | padding: EdgeInsets.only(top: 16.0), 23 | child: Icon(Ionicons.warning, size: 45, color: Color.fromARGB(255, 242, 193, 88)), 24 | ), 25 | const Text( 26 | "Are you sure?", 27 | style: TextStyle(fontWeight: FontWeight.w600, fontSize: 24.0), 28 | ), 29 | const Text( 30 | "all yo stuff will be deleted.", 31 | style: TextStyle(fontWeight: FontWeight.w500, fontSize: 18.0, color: Color.fromARGB(255, 206, 206, 206)), 32 | ), 33 | Padding( 34 | padding: const EdgeInsets.all(12.0), 35 | child: Row( 36 | children: [ 37 | Expanded( 38 | child: Padding( 39 | padding: const EdgeInsets.symmetric(horizontal: 4.0), 40 | child: TextButton.icon( 41 | icon: const Icon(Ionicons.checkmark), 42 | label: const Text("Yes!", style: TextStyle(fontWeight: FontWeight.w800)), 43 | style: ButtonStyle( 44 | padding: MaterialStateProperty.all(const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0)), 45 | backgroundColor: MaterialStateProperty.all(const Color.fromARGB(255, 242, 88, 88).withOpacity(.25)), 46 | foregroundColor: MaterialStateProperty.all(const Color.fromARGB(255, 242, 88, 88)), 47 | ), 48 | onPressed: () { 49 | Navigator.of(context, rootNavigator: true).pop('dialog'); 50 | Provider.of(context, listen: false).logoutCallback(); 51 | }, 52 | ), 53 | ), 54 | ), 55 | Expanded( 56 | child: Padding( 57 | padding: const EdgeInsets.symmetric(horizontal: 4.0), 58 | child: TextButton.icon( 59 | icon: const Icon(Ionicons.close), 60 | label: const Text("Nahh", style: TextStyle(fontWeight: FontWeight.w800)), 61 | style: ButtonStyle( 62 | padding: MaterialStateProperty.all(const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0)), 63 | backgroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.primary.withOpacity(.25)), 64 | foregroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.primary), 65 | ), 66 | onPressed: () { 67 | Navigator.of(context, rootNavigator: true).pop('dialog'); 68 | }, 69 | ), 70 | ), 71 | ), 72 | ], 73 | ), 74 | ), 75 | ], 76 | ), 77 | ), 78 | ), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/player/lyrics_view/rich_sync.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:tearmusic/models/music/lyrics.dart'; 4 | import 'package:tearmusic/providers/current_music_provider.dart'; 5 | 6 | Widget Function(BuildContext, int) richSyncListBuilder(List richSync) { 7 | return (context, index) { 8 | final currentMusic = context.read(); 9 | 10 | final richSyncLine = richSync[index]; 11 | double progress([Duration? o]) => 12 | (richSyncLine.start + (o ?? Duration.zero)).inMilliseconds / (currentMusic.duration?.inMilliseconds ?? 1); 13 | double progressEnd() => richSyncLine.end.inMilliseconds / (currentMusic.duration?.inMilliseconds ?? 1); 14 | 15 | return StreamBuilder( 16 | stream: currentMusic.positionStream, 17 | builder: (context, snapshot) { 18 | final value = snapshot.hasData && currentMusic.duration != null 19 | ? snapshot.data!.inMilliseconds / currentMusic.duration!.inMilliseconds 20 | : 0.0; 21 | return Padding( 22 | padding: const EdgeInsets.symmetric(horizontal: 12.0), 23 | child: InkWell( 24 | borderRadius: BorderRadius.circular(12.0), 25 | onTap: () { 26 | currentMusic.seek(richSyncLine.start); 27 | }, 28 | child: AnimatedContainer( 29 | duration: const Duration(milliseconds: 500), 30 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 14.0), 31 | decoration: BoxDecoration( 32 | color: value > progress() && value < progressEnd() ? Theme.of(context).colorScheme.secondary.withOpacity(.1) : Colors.transparent, 33 | borderRadius: BorderRadius.circular(12.0), 34 | ), 35 | child: Wrap( 36 | alignment: WrapAlignment.center, 37 | children: richSyncLine.segments 38 | .asMap() 39 | .map((i, e) { 40 | // if (actives[index][i] != (progress(e.offset) > animation.value)) { 41 | // actives[index][i] = progress(e.offset) > animation.value; 42 | // if (e.text.replaceAll(" ", "") != "") HapticFeedback.lightImpact(); 43 | // } 44 | 45 | return MapEntry( 46 | i, 47 | AnimatedDefaultTextStyle( 48 | duration: const Duration(milliseconds: 200), 49 | style: TextStyle( 50 | color: value > progress(e.offset) 51 | ? value < progressEnd() 52 | ? Theme.of(context).colorScheme.primary 53 | : Theme.of(context).colorScheme.secondary 54 | : Theme.of(context).colorScheme.secondary.withOpacity(.3), 55 | fontFamily: Theme.of(context).textTheme.bodyMedium!.fontFamily, 56 | fontWeight: FontWeight.bold, 57 | fontSize: 22.0, 58 | shadows: [ 59 | Shadow( 60 | offset: const Offset(5.0, 6.0), 61 | blurRadius: 0.0, 62 | color: Theme.of(context).colorScheme.secondary.withOpacity(value > progressEnd() ? .15 : 0), 63 | ), 64 | ], 65 | ), 66 | child: Text( 67 | e.text, 68 | textAlign: TextAlign.center, 69 | ), 70 | )); 71 | }) 72 | .values 73 | .toList(), 74 | ), 75 | ), 76 | ), 77 | ); 78 | }); 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/wallpaper.dart: -------------------------------------------------------------------------------- 1 | // import 'package:animated_background/animated_background.dart'; 2 | import 'package:flutter/material.dart'; 3 | // import 'package:provider/provider.dart'; 4 | // import 'package:tearmusic/models/segmented.dart'; 5 | // import 'package:tearmusic/providers/current_music_provider.dart'; 6 | 7 | // Disabled due to performance issues 8 | class Wallpaper extends StatefulWidget { 9 | const Wallpaper({Key? key, this.child, this.particleOpacity = .1, this.gradient = true}) : super(key: key); 10 | 11 | final Widget? child; 12 | final double particleOpacity; 13 | final bool gradient; 14 | 15 | @override 16 | State createState() => _WallpaperState(); 17 | } 18 | 19 | class _WallpaperState extends State with TickerProviderStateMixin { 20 | // static const double maxSpeed = 100.0; 21 | // static const double minBpm = 50.0; 22 | // static const double maxBpm = 200.0; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return widget.child ?? const SizedBox(); 27 | 28 | // final currentMusic = context.read(); 29 | // final tempo = context.select?>((p) => p.tma?.playbackHead?.tempo); 30 | 31 | // return MultiProvider( 32 | // providers: [ 33 | // StreamProvider( 34 | // create: (_) => currentMusic.player.positionStream.distinct(), 35 | // initialData: currentMusic.player.position, 36 | // ), 37 | // StreamProvider( 38 | // create: (_) => currentMusic.player.playingStream.distinct(), 39 | // initialData: currentMusic.player.playing, 40 | // ), 41 | // ], 42 | // builder: (context, child) => Consumer2( 43 | // builder: (context, pos, playing, child) { 44 | // final bpm = tempo?.firstWhere((e) => e.start > pos, orElse: () => tempo.last).bpm.clamp(minBpm, maxBpm); 45 | // double speed; 46 | 47 | // if (bpm != null) { 48 | // speed = Curves.easeInOutCubic.transform(((bpm - minBpm) / (maxBpm - minBpm)).clamp(0.0, 1.0)) * maxSpeed; 49 | // } else { 50 | // speed = 3; 51 | // } 52 | 53 | // final background = AnimatedBackground( 54 | // vsync: this, 55 | // behaviour: RandomParticleBehaviour( 56 | // options: ParticleOptions( 57 | // baseColor: Theme.of(context).colorScheme.tertiary, 58 | // spawnMaxRadius: 4, 59 | // spawnMinRadius: 2, 60 | // spawnMaxSpeed: speed, 61 | // spawnMinSpeed: (speed - 10).clamp(0.0, maxSpeed), 62 | // maxOpacity: widget.particleOpacity, 63 | // minOpacity: 0, 64 | // particleCount: 50, 65 | // ), 66 | // ), 67 | // child: const SizedBox(), 68 | // ); 69 | 70 | // return Scaffold( 71 | // resizeToAvoidBottomInset: false, 72 | // body: Stack( 73 | // children: [ 74 | // if (widget.gradient) 75 | // Container( 76 | // decoration: BoxDecoration( 77 | // gradient: RadialGradient( 78 | // center: const Alignment(0.95, -0.95), 79 | // radius: 1.0, 80 | // colors: [ 81 | // Theme.of(context).colorScheme.onSecondary.withOpacity(.3), 82 | // Theme.of(context).colorScheme.onSecondary.withOpacity(.2), 83 | // ], 84 | // ), 85 | // ), 86 | // ), 87 | // AnimatedOpacity( 88 | // duration: const Duration(seconds: 1), 89 | // opacity: playing ? 1 : 0, 90 | // child: background, 91 | // ), 92 | // if (widget.child != null) widget.child!, 93 | // ], 94 | // ), 95 | // ); 96 | // }, 97 | // ), 98 | // ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/ui/mobile/common/views/manual_match_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:loading_animation_widget/loading_animation_widget.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:tearmusic/models/manual_match.dart'; 6 | import 'package:tearmusic/models/music/track.dart'; 7 | import 'package:tearmusic/providers/current_music_provider.dart'; 8 | import 'package:tearmusic/providers/music_info_provider.dart'; 9 | import 'package:tearmusic/ui/mobile/common/tiles/manual_match_tile.dart'; 10 | 11 | class ManualMatchView extends StatefulWidget { 12 | const ManualMatchView(this.track, {Key? key}) : super(key: key); 13 | 14 | final MusicTrack track; 15 | 16 | static view(MusicTrack track, {required BuildContext context}) => 17 | Navigator.of(context, rootNavigator: true).push(CupertinoDialogRoute(context: context, builder: (context) => ManualMatchView(track))); 18 | 19 | @override 20 | State createState() => _ManualMatchViewState(); 21 | } 22 | 23 | class _ManualMatchViewState extends State { 24 | String? selected; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | context.read().pause(); 30 | } 31 | 32 | Future> getMatches() async { 33 | return await context.read().manualMatches(widget.track); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return AlertDialog( 39 | title: const Text( 40 | "Wrong song?", 41 | style: TextStyle(fontWeight: FontWeight.w600), 42 | ), 43 | content: FutureBuilder>( 44 | future: getMatches(), 45 | builder: (context, snapshot) { 46 | List? matches; 47 | 48 | if (snapshot.hasData) { 49 | matches = snapshot.data!.map((e) { 50 | return ManualMatchTile( 51 | e, 52 | selected: selected == e.videoId, 53 | onTap: () => setState(() => selected = e.videoId), 54 | ); 55 | }).toList(); 56 | } 57 | 58 | return SizedBox( 59 | height: MediaQuery.of(context).size.height / 2, 60 | width: 400, 61 | child: Column( 62 | crossAxisAlignment: CrossAxisAlignment.start, 63 | children: [ 64 | const Padding( 65 | padding: EdgeInsets.only(bottom: 12.0), 66 | child: Text("Try matching manually!"), 67 | ), 68 | if (!snapshot.hasData) 69 | Expanded( 70 | child: Center( 71 | child: LoadingAnimationWidget.staggeredDotsWave( 72 | color: Theme.of(context).colorScheme.secondary, 73 | size: 42.0, 74 | ), 75 | ), 76 | ), 77 | if (snapshot.hasData) 78 | Expanded( 79 | child: ListView( 80 | padding: EdgeInsets.zero, 81 | children: matches!, 82 | ), 83 | ), 84 | ], 85 | ), 86 | ); 87 | }, 88 | ), 89 | actions: [ 90 | ElevatedButton( 91 | onPressed: () { 92 | Navigator.of(context, rootNavigator: true).pop(); 93 | }, 94 | child: const Text("Cancel"), 95 | ), 96 | ElevatedButton( 97 | style: ElevatedButton.styleFrom( 98 | // Foreground color 99 | foregroundColor: Theme.of(context).colorScheme.onPrimary, 100 | // Background color 101 | backgroundColor: Theme.of(context).colorScheme.primary, 102 | ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), 103 | onPressed: () async { 104 | if (selected != null) { 105 | final api = context.read(); 106 | await api.purgeCache(widget.track); 107 | await api.matchManual(widget.track, selected!); 108 | // ignore: use_build_context_synchronously 109 | Navigator.of(context, rootNavigator: true).pop(); 110 | } 111 | }, 112 | child: const Text("Submit"), 113 | ), 114 | ], 115 | ); 116 | } 117 | } 118 | --------------------------------------------------------------------------------