├── android ├── settings_aar.gradle ├── app │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── drawable-hdpi │ │ │ │ │ ├── ic_clear.png │ │ │ │ │ ├── ic_pause.png │ │ │ │ │ ├── ic_stop.png │ │ │ │ │ ├── ic_play_arrow.png │ │ │ │ │ ├── ic_navigate_next.png │ │ │ │ │ ├── ic_navigate_before.png │ │ │ │ │ └── ic_stat_music_note.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ ├── ic_clear.png │ │ │ │ │ ├── ic_pause.png │ │ │ │ │ ├── ic_stop.png │ │ │ │ │ ├── ic_play_arrow.png │ │ │ │ │ ├── ic_navigate_next.png │ │ │ │ │ ├── ic_navigate_before.png │ │ │ │ │ └── ic_stat_music_note.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ ├── ic_clear.png │ │ │ │ │ ├── ic_pause.png │ │ │ │ │ ├── ic_stop.png │ │ │ │ │ ├── ic_play_arrow.png │ │ │ │ │ ├── ic_navigate_before.png │ │ │ │ │ ├── ic_navigate_next.png │ │ │ │ │ └── ic_stat_music_note.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ ├── ic_stop.png │ │ │ │ │ ├── ic_clear.png │ │ │ │ │ ├── ic_pause.png │ │ │ │ │ ├── ic_play_arrow.png │ │ │ │ │ ├── ic_navigate_next.png │ │ │ │ │ ├── ic_navigate_before.png │ │ │ │ │ └── ic_stat_music_note.png │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ ├── ic_clear.png │ │ │ │ │ ├── ic_pause.png │ │ │ │ │ ├── ic_stop.png │ │ │ │ │ ├── ic_play_arrow.png │ │ │ │ │ ├── ic_navigate_next.png │ │ │ │ │ ├── ic_navigate_before.png │ │ │ │ │ └── ic_stat_music_note.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ ├── launch_background.xml │ │ │ │ │ ├── ic_darktube.xml │ │ │ │ │ └── ic_lighttube.xml │ │ │ │ ├── drawable-night │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values │ │ │ │ │ └── styles.xml │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── .classpath │ ├── .project │ └── proguard-rules.pro ├── gradle.properties ├── .gitignore ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .settings │ └── org.eclipse.buildship.core.prefs ├── .project ├── settings.gradle └── build.gradle ├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcworkspace │ └── contents.xcworkspacedata ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme └── .gitignore ├── assets ├── images │ ├── logo.png │ ├── airis.png │ ├── github.png │ ├── appReady.png │ ├── appTheme.png │ ├── facebook.png │ ├── instagram.png │ ├── playArrow.png │ ├── telegram.png │ ├── grantAccess.png │ ├── ic_launcher.png │ ├── logo_christmas.png │ ├── youtube-music.png │ ├── artworkPlaceholder_big.png │ └── artworkPlaceholder_small.png └── fonts │ ├── productSans │ ├── bold.ttf │ └── regular.ttf │ ├── youtube-sans-bold.ttf │ ├── youtube-sans-light.ttf │ └── youtube-sans-medium.ttf ├── lib ├── internal │ ├── globals.dart │ ├── models │ │ ├── updateDetails.dart │ │ ├── folder.dart │ │ ├── videoFile.dart │ │ ├── streamSegmentTrack.dart │ │ ├── mediaItemSorts.dart │ │ ├── songFile.dart │ │ ├── subscription.dart │ │ └── tagsControllers.dart │ ├── download │ │ ├── audioFilters.dart │ │ └── tags.dart │ ├── randomString.dart │ ├── updateChecker.dart │ ├── nativeMethods.dart │ ├── lyricsProviders.dart │ ├── systemUi.dart │ ├── avatarHandler.dart │ └── database │ │ └── databaseService.dart ├── login │ ├── registerPage.dart │ └── loginPage.dart ├── players │ ├── components │ │ ├── youtubePlayer │ │ │ ├── player │ │ │ │ ├── gestures.dart │ │ │ │ └── playPauseButton.dart │ │ │ └── ui │ │ │ │ ├── fab.dart │ │ │ │ ├── details.dart │ │ │ │ └── engagement.dart │ │ ├── musicPlayer │ │ │ ├── playerPadding.dart │ │ │ ├── ui │ │ │ │ ├── randomButton.dart │ │ │ │ ├── repeatButton.dart │ │ │ │ ├── marqueeWidget.dart │ │ │ │ ├── playerBackground.dart │ │ │ │ └── playerSlider.dart │ │ │ └── expandedPanel.dart │ │ └── videoPlayer │ │ │ └── progressBar.dart │ └── service │ │ └── screenStateStream.dart ├── ui │ ├── internal │ │ ├── scrollBehavior.dart │ │ ├── scrollDetector.dart │ │ ├── lifecycleEvents.dart │ │ ├── popupMenu.dart │ │ └── snackbar.dart │ ├── animations │ │ ├── fadeSizeTransition.dart │ │ ├── routeAnimations.dart │ │ ├── FadeIn.dart │ │ ├── showUp.dart │ │ └── blurPageRoute.dart │ ├── dialogs │ │ ├── loadingDialog.dart │ │ ├── alertdialog.dart │ │ └── licenseDialog.dart │ ├── components │ │ ├── measureSize.dart │ │ ├── emptyIndicator.dart │ │ ├── textfieldTile.dart │ │ ├── shimmerContainer.dart │ │ ├── navigationBar.dart │ │ ├── searchBar.dart │ │ ├── autoHideScaffold.dart │ │ └── searchHistory.dart │ └── sheets │ │ ├── downloadFix.dart │ │ ├── disclaimer.dart │ │ ├── searchFilters.dart │ │ └── joinTelegram.dart ├── pages │ ├── components │ │ ├── channel │ │ │ └── bottom_play_all_sheet.dart │ │ ├── video │ │ │ └── shimmer │ │ │ │ ├── shimmerChannelLogo.dart │ │ │ │ ├── shimmerVideoComments.dart │ │ │ │ ├── shimmerArtworkEditor.dart │ │ │ │ ├── shimmerVideoEngagement.dart │ │ │ │ └── shimmerVideoTile.dart │ │ └── localVideos │ │ │ └── folderGridView.dart │ ├── watchHistory.dart │ └── localVideos.dart ├── screens │ ├── homeScreen │ │ └── pages │ │ │ ├── trending.dart │ │ │ ├── favorites.dart │ │ │ └── watchLater.dart │ ├── musicScreen │ │ ├── components │ │ │ ├── loadingListWidget.dart │ │ │ ├── playlistEmpty.dart │ │ │ ├── downloadsEmpty.dart │ │ │ ├── mediaListBase.dart │ │ │ └── noPermissionWidget.dart │ │ ├── dialogs │ │ │ ├── confirmDialog.dart │ │ │ └── optionsMenuDialog.dart │ │ └── tabs │ │ │ ├── songs.dart │ │ │ ├── genre.dart │ │ │ ├── artist.dart │ │ │ └── albums.dart │ ├── libraryScreen │ │ ├── socialLinksRow.dart │ │ └── components │ │ │ └── quickAcessTile.dart │ └── downloadScreen │ │ └── tabs │ │ ├── downloadsTab.dart │ │ ├── cancelledTab.dart │ │ └── queueTab.dart └── downloadMenu │ └── components │ └── loadingMenu.dart ├── .metadata ├── .gitignore ├── README.md └── LICENSE /android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/airis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/airis.png -------------------------------------------------------------------------------- /assets/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/github.png -------------------------------------------------------------------------------- /assets/images/appReady.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/appReady.png -------------------------------------------------------------------------------- /assets/images/appTheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/appTheme.png -------------------------------------------------------------------------------- /assets/images/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/facebook.png -------------------------------------------------------------------------------- /assets/images/instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/instagram.png -------------------------------------------------------------------------------- /assets/images/playArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/playArrow.png -------------------------------------------------------------------------------- /assets/images/telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/telegram.png -------------------------------------------------------------------------------- /android/app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /assets/images/grantAccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/grantAccess.png -------------------------------------------------------------------------------- /assets/images/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/ic_launcher.png -------------------------------------------------------------------------------- /assets/images/logo_christmas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/logo_christmas.png -------------------------------------------------------------------------------- /assets/images/youtube-music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/youtube-music.png -------------------------------------------------------------------------------- /assets/fonts/productSans/bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/fonts/productSans/bold.ttf -------------------------------------------------------------------------------- /assets/fonts/youtube-sans-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/fonts/youtube-sans-bold.ttf -------------------------------------------------------------------------------- /assets/fonts/productSans/regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/fonts/productSans/regular.ttf -------------------------------------------------------------------------------- /assets/fonts/youtube-sans-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/fonts/youtube-sans-light.ttf -------------------------------------------------------------------------------- /assets/fonts/youtube-sans-medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/fonts/youtube-sans-medium.ttf -------------------------------------------------------------------------------- /assets/images/artworkPlaceholder_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/artworkPlaceholder_big.png -------------------------------------------------------------------------------- /assets/images/artworkPlaceholder_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/assets/images/artworkPlaceholder_small.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-hdpi/ic_clear.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-hdpi/ic_pause.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-hdpi/ic_stop.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-mdpi/ic_clear.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-mdpi/ic_pause.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-mdpi/ic_stop.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xhdpi/ic_clear.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xhdpi/ic_pause.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xhdpi/ic_stop.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_stop.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/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/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_clear.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_pause.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_clear.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_pause.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_stop.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/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/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_play_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-hdpi/ic_play_arrow.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_play_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-mdpi/ic_play_arrow.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_play_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xhdpi/ic_play_arrow.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_navigate_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-hdpi/ic_navigate_next.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_navigate_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-mdpi/ic_navigate_next.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_play_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_play_arrow.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_play_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_play_arrow.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_navigate_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-hdpi/ic_navigate_before.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_stat_music_note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-hdpi/ic_stat_music_note.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_navigate_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-mdpi/ic_navigate_before.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_stat_music_note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-mdpi/ic_stat_music_note.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_navigate_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xhdpi/ic_navigate_before.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_navigate_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xhdpi/ic_navigate_next.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_stat_music_note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xhdpi/ic_stat_music_note.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_navigate_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_navigate_next.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_navigate_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_navigate_next.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_navigate_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_navigate_before.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_stat_music_note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_stat_music_note.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_navigate_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_navigate_before.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_stat_music_note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_stat_music_note.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwema3/SongTubeExactApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /lib/internal/globals.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | // ------------------ 4 | // Shared Preferences 5 | // ------------------ 6 | SharedPreferences globalPrefs; -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/login/registerPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class STRegisterPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Container( 7 | 8 | ); 9 | } 10 | } -------------------------------------------------------------------------------- /lib/players/components/youtubePlayer/player/gestures.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class PlayerGestures extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Container( 7 | 8 | ); 9 | } 10 | } -------------------------------------------------------------------------------- /lib/ui/internal/scrollBehavior.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomScrollBehavior extends ScrollBehavior { 4 | @override 5 | Widget buildViewportChrome( 6 | BuildContext context, Widget child, AxisDirection axisDirection) { 7 | return child; 8 | } 9 | } -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /lib/login/loginPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class STLoginPage extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return Container( 9 | 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/internal/models/updateDetails.dart: -------------------------------------------------------------------------------- 1 | class UpdateDetails { 2 | 3 | String version; 4 | String publishDate; 5 | String updateDetails; 6 | String downloadUrl; 7 | 8 | UpdateDetails( 9 | this.version, 10 | this.publishDate, 11 | this.updateDetails, 12 | this.downloadUrl 13 | ); 14 | 15 | } -------------------------------------------------------------------------------- /lib/internal/download/audioFilters.dart: -------------------------------------------------------------------------------- 1 | class AudioFilters { 2 | 3 | double volume; 4 | int bassGain; 5 | int trebleGain; 6 | bool normalizeAudio; 7 | 8 | AudioFilters({ 9 | this.volume = 1, 10 | this.bassGain = 0, 11 | this.trebleGain = 0, 12 | this.normalizeAudio = false 13 | }); 14 | 15 | } -------------------------------------------------------------------------------- /lib/internal/models/folder.dart: -------------------------------------------------------------------------------- 1 | // Internal 2 | import 'package:songtube/internal/models/videoFile.dart'; 3 | 4 | class FolderItem { 5 | 6 | String name; 7 | String path; 8 | List videos; 9 | 10 | FolderItem({ 11 | this.name, 12 | this.path, 13 | }) { 14 | videos = []; 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /.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 and should not be manually edited. 5 | 6 | version: 7 | revision: f139b11009aeb8ed2a3a3aa8b0066e482709dde3 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /lib/internal/download/tags.dart: -------------------------------------------------------------------------------- 1 | class DownloadTags { 2 | 3 | String title; 4 | String album; 5 | String artist; 6 | String genre; 7 | String coverurl; 8 | String date; 9 | String disc; 10 | String track; 11 | 12 | DownloadTags({this.title, this.album, this.artist, this.genre, 13 | this.coverurl, this.date, this.disc, this.track}); 14 | 15 | } -------------------------------------------------------------------------------- /lib/internal/models/videoFile.dart: -------------------------------------------------------------------------------- 1 | class VideoFile { 2 | 3 | String name; 4 | String path; 5 | DateTime lastModified; 6 | String thumbnail; 7 | String duration; 8 | String size; 9 | 10 | VideoFile({ 11 | this.name, 12 | this.path, 13 | this.lastModified, 14 | this.thumbnail, 15 | this.duration, 16 | this.size 17 | }); 18 | 19 | } -------------------------------------------------------------------------------- /lib/internal/models/streamSegmentTrack.dart: -------------------------------------------------------------------------------- 1 | import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; 2 | import 'package:songtube/internal/models/tagsControllers.dart'; 3 | 4 | class StreamSegmentTrack { 5 | 6 | StreamSegment segment; 7 | TagsControllers tags; 8 | bool selected; 9 | 10 | StreamSegmentTrack(this.segment, this.tags, this.selected); 11 | 12 | } -------------------------------------------------------------------------------- /android/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=C\:/Program Files/Java/jdk1.8.0_241 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/pages/components/channel/bottom_play_all_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ChannelPageBottomSheet extends StatelessWidget { 4 | const ChannelPageBottomSheet({ 5 | @required this.onPlayAll, 6 | @required this.onDownloadAll, 7 | Key key }) : super(key: key); 8 | final Function() onPlayAll; 9 | final Function() onDownloadAll; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Container( 14 | 15 | ); 16 | } 17 | } -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/screens/homeScreen/pages/trending.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:songtube/provider/managerProvider.dart'; 4 | import 'package:songtube/ui/layout/streamsLargeThumbnail.dart'; 5 | 6 | class HomePageTrending extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | ManagerProvider manager = Provider.of(context); 10 | return StreamsLargeThumbnailView( 11 | infoItems: manager.homeTrendingVideoList 12 | ); 13 | } 14 | } -------------------------------------------------------------------------------- /lib/ui/animations/fadeSizeTransition.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FadeSizeTransition extends StatelessWidget { 4 | final Animation animator; 5 | final Widget child; 6 | FadeSizeTransition({ 7 | @required this.animator, 8 | @required this.child 9 | }); 10 | @override 11 | Widget build(BuildContext context) { 12 | return FadeTransition( 13 | opacity: animator, 14 | child: SizeTransition( 15 | sizeFactor: animator, 16 | child: child, 17 | ), 18 | ); 19 | } 20 | } -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /lib/internal/randomString.dart: -------------------------------------------------------------------------------- 1 | // Dart 2 | import 'dart:math'; 3 | 4 | class RandomString { 5 | static const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; 6 | static const _letters = 'qwertyuiopasdfghjlcvbnm'; 7 | static String getRandomString(int length) => String.fromCharCodes(Iterable.generate( 8 | length, (_) => _chars.codeUnitAt(Random().nextInt(_chars.length)))); 9 | static String getRandomLetter() => String.fromCharCodes(Iterable.generate( 10 | 1, (_) => _letters 11 | .codeUnitAt(Random().nextInt(_letters.length)) 12 | )); 13 | } -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /android/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.4.10' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } -------------------------------------------------------------------------------- /lib/pages/components/video/shimmer/shimmerChannelLogo.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shimmer/shimmer.dart'; 3 | 4 | class ShimmerChannelLogo extends StatelessWidget { 5 | const ShimmerChannelLogo(); 6 | @override 7 | Widget build(BuildContext context) { 8 | return Shimmer.fromColors( 9 | baseColor: Theme.of(context).cardColor.withOpacity(0.4), 10 | highlightColor: Theme.of(context).cardColor, 11 | child: Container( 12 | width: 60, 13 | height: 60, 14 | decoration: BoxDecoration( 15 | borderRadius: BorderRadius.circular(50), 16 | color: Theme.of(context).cardColor.withOpacity(0.4) 17 | ), 18 | ), 19 | ); 20 | } 21 | } -------------------------------------------------------------------------------- /lib/internal/models/mediaItemSorts.dart: -------------------------------------------------------------------------------- 1 | import 'package:audio_service/audio_service.dart'; 2 | 3 | class MediaItemAlbum { 4 | 5 | String albumTitle; 6 | String albumAuthor; 7 | List mediaItems; 8 | 9 | MediaItemAlbum({ 10 | this.albumAuthor, 11 | this.albumTitle, 12 | this.mediaItems 13 | }); 14 | 15 | } 16 | 17 | class MediaItemArtist { 18 | 19 | String artistName; 20 | List mediaItems; 21 | 22 | MediaItemArtist({ 23 | this.artistName, 24 | this.mediaItems 25 | }); 26 | 27 | } 28 | 29 | class MediaItemGenre { 30 | 31 | String genreName; 32 | List mediaItems; 33 | 34 | MediaItemGenre({ 35 | this.genreName, 36 | this.mediaItems 37 | }); 38 | 39 | } -------------------------------------------------------------------------------- /lib/players/service/screenStateStream.dart: -------------------------------------------------------------------------------- 1 | // Internal 2 | import 'package:songtube/players/service/playerService.dart'; 3 | 4 | // Packages 5 | import 'package:audio_service/audio_service.dart'; 6 | import 'package:rxdart/rxdart.dart'; 7 | 8 | /// Encapsulate all the different data we're interested in into a single 9 | /// stream so we don't have to nest StreamBuilders. 10 | Stream get screenStateStream => 11 | Rx.combineLatest3, MediaItem, PlaybackState, ScreenState>( 12 | AudioService.queueStream, 13 | AudioService.currentMediaItemStream, 14 | AudioService.playbackStateStream, 15 | (queue, mediaItem, playbackState) => 16 | ScreenState(queue, mediaItem, playbackState)); -------------------------------------------------------------------------------- /lib/ui/dialogs/loadingDialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoadingDialog extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Container( 7 | height: 50, 8 | width: 50, 9 | child: Dialog( 10 | shape: RoundedRectangleBorder( 11 | borderRadius: BorderRadius.circular(20) 12 | ), 13 | backgroundColor: Colors.transparent, 14 | elevation: 0, 15 | child: Container( 16 | height: 100, 17 | width: 100, 18 | child: Center( 19 | child: CircularProgressIndicator( 20 | valueColor: AlwaysStoppedAnimation(Colors.white), 21 | ), 22 | ), 23 | ) 24 | ), 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 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | 39 | # Signing 40 | songtube.jks 41 | key.properties -------------------------------------------------------------------------------- /lib/ui/animations/routeAnimations.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | class FadeRoute extends PageRouteBuilder { 5 | final Widget page; 6 | FadeRoute({this.page}) 7 | : super( 8 | pageBuilder: ( 9 | BuildContext context, 10 | Animation animation, 11 | Animation secondaryAnimation, 12 | ) => 13 | page, 14 | transitionDuration: Duration(milliseconds: 300), 15 | transitionsBuilder: ( 16 | BuildContext context, 17 | Animation animation, 18 | Animation secondaryAnimation, 19 | Widget child, 20 | ) => 21 | FadeTransition( 22 | opacity: animation, 23 | child: child, 24 | ), 25 | ); 26 | } -------------------------------------------------------------------------------- /lib/players/components/youtubePlayer/player/playPauseButton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class PlayPauseButton extends StatelessWidget { 4 | final bool isPlaying; 5 | final Function onPlayPause; 6 | PlayPauseButton({ 7 | @required this.isPlaying, 8 | @required this.onPlayPause 9 | }); 10 | @override 11 | Widget build(BuildContext context) { 12 | return InkWell( 13 | onTap: onPlayPause, 14 | borderRadius: BorderRadius.circular(100), 15 | child: Ink( 16 | padding: const EdgeInsets.all(16.0), 17 | child: isPlaying 18 | ? Icon( 19 | Icons.pause, 20 | size: 32, 21 | color: Colors.white, 22 | ) 23 | : Icon( 24 | Icons.play_arrow, 25 | size: 32, 26 | color: Colors.white, 27 | ), 28 | ), 29 | ); 30 | } 31 | } -------------------------------------------------------------------------------- /lib/downloadMenu/components/loadingMenu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoadingDownloadMenu extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Row( 7 | mainAxisAlignment: MainAxisAlignment.start, 8 | crossAxisAlignment: CrossAxisAlignment.center, 9 | children: [ 10 | SizedBox(width: 16), 11 | CircularProgressIndicator( 12 | valueColor: AlwaysStoppedAnimation( 13 | Theme.of(context).accentColor 14 | ), 15 | ), 16 | SizedBox(width: 16), 17 | Text( 18 | "Loading...", 19 | style: TextStyle( 20 | fontFamily: 'YTSans', 21 | color: Theme.of(context).textTheme.bodyText1.color, 22 | fontWeight: FontWeight.w600, 23 | fontSize: 18 24 | ), 25 | ) 26 | ], 27 | ); 28 | } 29 | } -------------------------------------------------------------------------------- /lib/ui/internal/scrollDetector.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ScrollDetector extends StatelessWidget { 4 | final Function onScrollUp; 5 | final Function onScrollDown; 6 | final Widget child; 7 | ScrollDetector({ 8 | @required this.child, 9 | this.onScrollDown, 10 | this.onScrollUp 11 | }); 12 | final sensitivityFactor = 5; 13 | @override 14 | Widget build(BuildContext context) { 15 | return NotificationListener( 16 | onNotification: (ScrollUpdateNotification details) { 17 | if (details.scrollDelta.abs() < sensitivityFactor) 18 | return false; 19 | if (details.scrollDelta > 0.0 && details.metrics.axis == Axis.vertical) { 20 | onScrollDown(); 21 | } else { 22 | onScrollUp(); 23 | } 24 | return false; 25 | }, 26 | child: child, 27 | ); 28 | } 29 | } -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/internal/updateChecker.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:http/http.dart' as http; 3 | import 'package:songtube/internal/models/updateDetails.dart'; 4 | 5 | Future getLatestRelease() async { 6 | try { 7 | var client = http.Client(); 8 | var headers = { 9 | "Accept": "application/vnd.github.v3+json" 10 | }; 11 | var response = await client 12 | .get(Uri.parse("https://api.github.com/repos/SongTube/SongTube-App/releases"), 13 | headers: headers); 14 | var jsonResponse = jsonDecode(response.body); 15 | UpdateDetails details = UpdateDetails( 16 | jsonResponse[0]["tag_name"], 17 | jsonResponse[0]["published_at"].split("T").first, 18 | jsonResponse[0]["body"], 19 | jsonResponse[0]["assets"][0]["browser_download_url"] 20 | ); 21 | client.close(); 22 | return details; 23 | } catch (_) { 24 | return null; 25 | } 26 | } 27 | 28 | Future queryUpdate() async { 29 | 30 | } -------------------------------------------------------------------------------- /lib/players/components/youtubePlayer/ui/fab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class VideoDownloadFab extends StatelessWidget { 4 | final bool readyToDownload; 5 | final Function onDownload; 6 | VideoDownloadFab({ 7 | @required this.readyToDownload, 8 | @required this.onDownload, 9 | }); 10 | @override 11 | Widget build(BuildContext context) { 12 | return FloatingActionButton( 13 | child: AnimatedSwitcher( 14 | duration: Duration(milliseconds: 400), 15 | child: readyToDownload 16 | ? Icon(Icons.file_download) 17 | : CircularProgressIndicator( 18 | backgroundColor: Theme.of(context).accentColor, 19 | valueColor: AlwaysStoppedAnimation(Colors.white), 20 | ), 21 | ), 22 | backgroundColor: Theme.of(context).accentColor, 23 | foregroundColor: Colors.white, 24 | onPressed: () { 25 | if (readyToDownload) { 26 | onDownload(); 27 | } 28 | } 29 | ); 30 | } 31 | } -------------------------------------------------------------------------------- /lib/pages/components/video/shimmer/shimmerVideoComments.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shimmer/shimmer.dart'; 3 | 4 | class ShimmerVideoComments extends StatelessWidget { 5 | const ShimmerVideoComments(); 6 | @override 7 | Widget build(BuildContext context) { 8 | return Container( 9 | height: 60, 10 | margin: EdgeInsets.only(left: 12), 11 | child: Center( 12 | child: Align( 13 | alignment: Alignment.centerLeft, 14 | child: Shimmer.fromColors( 15 | baseColor: Theme.of(context).cardColor.withOpacity(0.4), 16 | highlightColor: Theme.of(context).cardColor, 17 | child: Container( 18 | width: 150, 19 | height: 50, 20 | decoration: BoxDecoration( 21 | borderRadius: BorderRadius.circular(50), 22 | color: Theme.of(context).cardColor.withOpacity(0.4) 23 | ), 24 | ), 25 | ), 26 | ), 27 | ), 28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /lib/screens/musicScreen/components/loadingListWidget.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | import 'package:songtube/internal/languages.dart'; 4 | 5 | class MediaLoadingWidget extends StatelessWidget { 6 | const MediaLoadingWidget(); 7 | @override 8 | Widget build(BuildContext context) { 9 | return Center( 10 | child: Column( 11 | mainAxisAlignment: MainAxisAlignment.center, 12 | children: [ 13 | CircularProgressIndicator( 14 | backgroundColor: Theme.of(context).scaffoldBackgroundColor, 15 | valueColor: AlwaysStoppedAnimation(Theme.of(context).accentColor), 16 | ), 17 | Container( 18 | margin: EdgeInsets.only(top: 16), 19 | child: Text( 20 | Languages.of(context).labelGettingYourMedia, 21 | style: TextStyle( 22 | fontFamily: 'YTSans', 23 | fontSize: 20 24 | ), 25 | ), 26 | ) 27 | ], 28 | ), 29 | ); 30 | } 31 | } -------------------------------------------------------------------------------- /lib/ui/dialogs/alertdialog.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | class CustomAlert extends StatelessWidget { 5 | final Icon leadingIcon; 6 | final String title; 7 | final String content; 8 | final List actions; 9 | CustomAlert({ 10 | this.leadingIcon, 11 | @required this.title, 12 | @required this.content, 13 | @required this.actions 14 | }); 15 | @override 16 | Widget build(BuildContext context) { 17 | return AlertDialog( 18 | shape: RoundedRectangleBorder( 19 | borderRadius: BorderRadius.circular(10), 20 | ), 21 | title: Row( 22 | children: [ 23 | leadingIcon, 24 | SizedBox(width: 6), 25 | Text(title, style: TextStyle( 26 | color: Theme.of(context).textTheme.bodyText1.color 27 | )), 28 | ], 29 | ), 30 | content: Text(content, style: TextStyle( 31 | color: Theme.of(context).textTheme.bodyText1.color 32 | )), 33 | actions: actions 34 | ); 35 | } 36 | } -------------------------------------------------------------------------------- /lib/ui/internal/lifecycleEvents.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | // Add this to any Widget in the App to detect if 6 | // the app is inactive, paused, detached or resumed 7 | class LifecycleEventHandler extends WidgetsBindingObserver { 8 | final AsyncCallback resumeCallBack; 9 | final AsyncCallback suspendingCallBack; 10 | 11 | LifecycleEventHandler({ 12 | this.resumeCallBack, 13 | this.suspendingCallBack 14 | }); 15 | 16 | @override 17 | Future didChangeAppLifecycleState(AppLifecycleState state) async { 18 | switch (state) { 19 | case AppLifecycleState.resumed: 20 | if (resumeCallBack != null) { 21 | await resumeCallBack(); 22 | } 23 | break; 24 | case AppLifecycleState.inactive: 25 | case AppLifecycleState.paused: 26 | case AppLifecycleState.detached: 27 | if (suspendingCallBack != null) { 28 | await suspendingCallBack(); 29 | } 30 | break; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_darktube.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_lighttube.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://i.imgur.com/Y80SpfK.jpg) 2 | 3 | SongTube is a new beautiful and fast application made in flutter, it supports YouTube audio and video downloading at any quality, In-App YouTube Browser, audio conversion, Playlists and an Audio tags editor. 4 | 5 | --- 6 | 7 | ## Features 8 | 9 | + Video Download at any available Quality 10 | + Download HDR and 60fps Videos 11 | + Audio Download at best available Quality 12 | + Audio Tags & Artwork Editor 13 | + Audio Filters (Volume, Bass, Treble) 14 | + Audio Conversion (AAC, OGG and MP3) (optional) 15 | + Full Playlist Downloads (Only Audio) 16 | + Set custom path for Audio/Video download 17 | + Music Player built-in 18 | + Video Player built-in 19 | + Music Equalizer 20 | + Music Playlists 21 | + Youtube Videos Playlists 22 | + In-App Youtube Browser 23 | + Light/Dark/Black Themes 24 | + Accent Color Picker 25 | + UI Customizations 26 | 27 | --- 28 | 29 | ## Developer's Info 30 | 31 | >Bagirishya Rwema Dominique 32 | 33 | - Twitter: (Click [@Here](https://twitter.com/R_w_e_m_a)) 34 | - GitHub Page (Click [@Here](https://github.com/rwema3)) 35 | 36 | -------------------------------------------------------------------------------- /lib/ui/components/measureSize.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | 4 | typedef void OnWidgetSizeChange(Size size); 5 | 6 | class MeasureSize extends StatefulWidget { 7 | final Widget child; 8 | final OnWidgetSizeChange onChange; 9 | 10 | const MeasureSize({ 11 | Key key, 12 | @required this.onChange, 13 | @required this.child, 14 | }) : super(key: key); 15 | 16 | @override 17 | _MeasureSizeState createState() => _MeasureSizeState(); 18 | } 19 | 20 | class _MeasureSizeState extends State { 21 | @override 22 | Widget build(BuildContext context) { 23 | SchedulerBinding.instance.addPostFrameCallback(postFrameCallback); 24 | return Container( 25 | key: widgetKey, 26 | child: widget.child, 27 | ); 28 | } 29 | 30 | var widgetKey = GlobalKey(); 31 | var oldSize; 32 | 33 | void postFrameCallback(_) { 34 | var context = widgetKey.currentContext; 35 | if (context == null) return; 36 | 37 | var newSize = context.size; 38 | if (oldSize == newSize) return; 39 | 40 | oldSize = newSize; 41 | widget.onChange(newSize); 42 | } 43 | } -------------------------------------------------------------------------------- /lib/players/components/musicPlayer/playerPadding.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | // Internal 5 | import 'package:songtube/players/service/screenStateStream.dart'; 6 | import 'package:songtube/players/service/playerService.dart'; 7 | 8 | // Packages 9 | import 'package:audio_service/audio_service.dart'; 10 | 11 | class MusicPlayerPadding extends StatelessWidget { 12 | final bool searchBarOpen; 13 | MusicPlayerPadding(this.searchBarOpen); 14 | @override 15 | Widget build(BuildContext context) { 16 | return StreamBuilder( 17 | stream: screenStateStream, 18 | builder: (context, snapshot) { 19 | final screenState = snapshot.data; 20 | final state = screenState?.playbackState; 21 | final processingState = 22 | state?.processingState ?? AudioProcessingState.none; 23 | return Container( 24 | height: searchBarOpen 25 | ? 0 : processingState == AudioProcessingState.stopped || 26 | processingState == AudioProcessingState.none 27 | ? 0 28 | : kToolbarHeight * 1.15 29 | ); 30 | } 31 | ); 32 | } 33 | } -------------------------------------------------------------------------------- /lib/screens/musicScreen/dialogs/confirmDialog.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | class ConfirmDialog extends StatelessWidget { 5 | final Function onConfirm; 6 | final Function onCancel; 7 | ConfirmDialog({ 8 | @required this.onConfirm, 9 | @required this.onCancel 10 | }); 11 | @override 12 | Widget build(BuildContext context) { 13 | return AlertDialog( 14 | shape: RoundedRectangleBorder( 15 | borderRadius: BorderRadius.circular(10) 16 | ), 17 | title: Text( 18 | "Are you sure?", 19 | style: TextStyle(color: Theme.of(context).textTheme.bodyText1.color) 20 | ), 21 | content: Text( 22 | "You are going to permanently delete this Song", 23 | style: TextStyle(color: Theme.of(context).textTheme.bodyText1.color) 24 | ), 25 | actions: [ 26 | TextButton( 27 | child: Text("OK", style: TextStyle(color: Theme.of(context).accentColor)), 28 | onPressed: onConfirm 29 | ), 30 | TextButton( 31 | child: Text("Cancel", style: TextStyle(color: Theme.of(context).accentColor)), 32 | onPressed: onCancel, 33 | ) 34 | ], 35 | ); 36 | } 37 | } -------------------------------------------------------------------------------- /lib/screens/libraryScreen/socialLinksRow.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:url_launcher/url_launcher.dart'; 3 | 4 | class SocialLinksRow extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return Container( 8 | margin: EdgeInsets.only(left: 12, right: 12, top: 16), 9 | height: 50, 10 | child: Row( 11 | mainAxisAlignment: MainAxisAlignment.spaceAround, 12 | children: [ 13 | GestureDetector( 14 | onTap: () => launch("https://t.me/songtubechannel"), 15 | child: Image.asset('assets/images/telegram.png') 16 | ), 17 | GestureDetector( 18 | onTap: () => launch("https://github.com/SongTube"), 19 | child: Image.asset('assets/images/github.png') 20 | ), 21 | GestureDetector( 22 | onTap: () => launch("https://facebook.com/songtubeapp/"), 23 | child: Image.asset('assets/images/facebook.png') 24 | ), 25 | GestureDetector( 26 | onTap: () => launch("https://instagram.com/songtubeapp"), 27 | child: Image.asset('assets/images/instagram.png') 28 | ), 29 | ], 30 | ), 31 | ); 32 | } 33 | } -------------------------------------------------------------------------------- /lib/screens/downloadScreen/tabs/downloadsTab.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | // Internal 5 | import 'package:songtube/provider/mediaProvider.dart'; 6 | import 'package:songtube/screens/musicScreen/components/mediaListBase.dart'; 7 | 8 | // Packages 9 | import 'package:provider/provider.dart'; 10 | import 'package:songtube/screens/musicScreen/components/songsList.dart'; 11 | 12 | // UI 13 | import 'package:songtube/screens/musicScreen/tabs/songs.dart'; 14 | 15 | class DownloadsTab extends StatefulWidget { 16 | @override 17 | _DownloadsTabState createState() => _DownloadsTabState(); 18 | } 19 | 20 | class _DownloadsTabState extends State { 21 | 22 | @override 23 | void initState() { 24 | Provider.of(context, listen: false).getDatabase(); 25 | super.initState(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | MediaProvider mediaProvider = Provider.of(context); 31 | return MediaListBase( 32 | isLoading: mediaProvider.loadingDownloads, 33 | isEmpty: mediaProvider.databaseSongs.isEmpty, 34 | listType: MediaListBaseType.Downloads, 35 | child: SongsListView( 36 | songs: mediaProvider.databaseSongs, 37 | hasDownloadType: true, 38 | ), 39 | ); 40 | } 41 | } -------------------------------------------------------------------------------- /lib/screens/musicScreen/dialogs/optionsMenuDialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:audio_service/audio_service.dart'; 2 | import 'package:eva_icons_flutter/eva_icons_flutter.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:songtube/internal/languages.dart'; 5 | 6 | enum DeleteFrom { downloads, music } 7 | 8 | class MediaOptionsMenuDialog extends StatelessWidget { 9 | final MediaItem song; 10 | final Function onDelete; 11 | MediaOptionsMenuDialog({ 12 | @required this.song, 13 | @required this.onDelete 14 | }); 15 | @override 16 | Widget build(BuildContext context) { 17 | return AlertDialog( 18 | shape: RoundedRectangleBorder( 19 | borderRadius: BorderRadius.circular(20) 20 | ), 21 | content: Column( 22 | mainAxisSize: MainAxisSize.min, 23 | children: [ 24 | ListTile( 25 | leading: Icon(EvaIcons.trashOutline, color: Theme.of(context).accentColor), 26 | title: Text( 27 | Languages.of(context).labelDeleteSong, 28 | style: TextStyle( 29 | fontFamily: 'YTSans', 30 | color: Theme.of(context).textTheme.bodyText1.color.withOpacity(0.6) 31 | ), 32 | ), 33 | onTap: onDelete 34 | ), 35 | ], 36 | ), 37 | ); 38 | } 39 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /lib/ui/dialogs/licenseDialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart' show rootBundle; 3 | 4 | class LicenseDialog extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return AlertDialog( 8 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), 9 | title: Text("LICENSE", style: TextStyle( 10 | color: Theme.of(context).textTheme.bodyText1.color, 11 | )), 12 | content: FutureBuilder( 13 | future: getLicense(), 14 | builder: (context, AsyncSnapshot license) { 15 | if (license.hasData) { 16 | return SingleChildScrollView( 17 | child: Text(license.data, style: TextStyle( 18 | color: Theme.of(context).textTheme.bodyText1.color, 19 | )), 20 | ); 21 | } else { 22 | return Center(child: CircularProgressIndicator()); 23 | } 24 | }, 25 | ), 26 | actions: [ 27 | TextButton( 28 | onPressed: () { 29 | Navigator.pop(context); 30 | }, 31 | child: Text("OK", style: TextStyle( 32 | color: Theme.of(context).accentColor, 33 | )), 34 | ) 35 | ], 36 | ); 37 | } 38 | 39 | Future getLicense() async { 40 | return await rootBundle.loadString('assets/LICENSE'); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /lib/ui/components/emptyIndicator.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | import 'package:songtube/internal/languages.dart'; 4 | import 'package:songtube/ui/animations/showUp.dart'; 5 | 6 | class EmptyIndicator extends StatelessWidget { 7 | const EmptyIndicator(); 8 | @override 9 | Widget build(BuildContext context) { 10 | return ShowUpTransition( 11 | duration: Duration(milliseconds: 600), 12 | delay: Duration(milliseconds: 400), 13 | forward: true, 14 | slideSide: SlideFromSlide.TOP, 15 | child: Container( 16 | height: 30, 17 | margin: EdgeInsets.all(16), 18 | width: double.infinity, 19 | decoration: BoxDecoration( 20 | borderRadius: BorderRadius.circular(10), 21 | color: Theme.of(context).accentColor, 22 | boxShadow: [ 23 | BoxShadow( 24 | color: Colors.black.withOpacity(0.2), 25 | offset: Offset(3,3), 26 | blurRadius: 8, 27 | spreadRadius: 1 28 | ) 29 | ] 30 | ), 31 | child: Center( 32 | child: Text( 33 | Languages.of(context).labelEmpty, 34 | style: TextStyle( 35 | color: Colors.white, 36 | fontWeight: FontWeight.w600, 37 | fontFamily: 'Product Sans', 38 | fontSize: 14, 39 | ), 40 | ), 41 | ), 42 | ), 43 | ); 44 | } 45 | } -------------------------------------------------------------------------------- /lib/screens/musicScreen/tabs/songs.dart: -------------------------------------------------------------------------------- 1 | import 'package:audio_service/audio_service.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:songtube/provider/mediaProvider.dart'; 6 | import 'package:songtube/screens/musicScreen/components/songsList.dart'; 7 | 8 | 9 | class MusicScreenSongsTab extends StatefulWidget { 10 | final List songs; 11 | final bool hasDownloadType; 12 | final String searchQuery; 13 | MusicScreenSongsTab({ 14 | @required this.songs, 15 | this.hasDownloadType = false, 16 | this.searchQuery = "" 17 | }); 18 | 19 | @override 20 | State createState() => _MusicScreenSongsTabState(); 21 | } 22 | 23 | class _MusicScreenSongsTabState extends State { 24 | 25 | // Scroll Controller 26 | ScrollController controller; 27 | 28 | @override 29 | void initState() { 30 | controller = ScrollController(initialScrollOffset: 31 | Provider.of(context, listen: false).musicScrollPosition); 32 | controller.addListener(() { 33 | Provider.of(context, listen: false) 34 | .musicScrollPosition = controller.position.pixels; 35 | }); 36 | super.initState(); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return SongsListView( 42 | scrollController: controller, 43 | songs: widget.songs, searchQuery: widget.searchQuery); 44 | } 45 | } -------------------------------------------------------------------------------- /lib/screens/musicScreen/components/playlistEmpty.dart: -------------------------------------------------------------------------------- 1 | import 'package:eva_icons_flutter/eva_icons_flutter.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:songtube/internal/languages.dart'; 4 | import 'package:songtube/ui/animations/showUp.dart'; 5 | 6 | class PlaylistEmptyWidget extends StatelessWidget { 7 | const PlaylistEmptyWidget(); 8 | @override 9 | Widget build(BuildContext context) { 10 | return ShowUpTransition( 11 | duration: Duration(milliseconds: 400), 12 | slideSide: SlideFromSlide.BOTTOM, 13 | forward: true, 14 | child: Container( 15 | padding: EdgeInsets.all(8), 16 | height: 240, 17 | width: MediaQuery.of(context).size.width*0.6, 18 | child: Padding( 19 | padding: const EdgeInsets.all(24.0), 20 | child: Column( 21 | mainAxisSize: MainAxisSize.min, 22 | children: [ 23 | Icon(EvaIcons.loaderOutline, size: 100, color: Theme.of(context).accentColor), 24 | SizedBox(height: 8), 25 | Text( 26 | Languages.of(context).labelNoMediaYet, 27 | style: TextStyle( 28 | fontWeight: FontWeight.w700, 29 | fontFamily: 'Varela' 30 | ), 31 | ), 32 | SizedBox(height: 4), 33 | Text(Languages.of(context).labelNoMediaYetJustification, 34 | textAlign: TextAlign.center), 35 | ], 36 | ), 37 | ), 38 | ), 39 | ); 40 | } 41 | } -------------------------------------------------------------------------------- /lib/screens/musicScreen/components/downloadsEmpty.dart: -------------------------------------------------------------------------------- 1 | import 'package:eva_icons_flutter/eva_icons_flutter.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:songtube/internal/languages.dart'; 4 | import 'package:songtube/ui/animations/showUp.dart'; 5 | 6 | class MediaDownloadsEmpty extends StatelessWidget { 7 | const MediaDownloadsEmpty(); 8 | @override 9 | Widget build(BuildContext context) { 10 | return ShowUpTransition( 11 | duration: Duration(milliseconds: 400), 12 | slideSide: SlideFromSlide.BOTTOM, 13 | forward: true, 14 | child: Container( 15 | padding: EdgeInsets.all(8), 16 | height: 240, 17 | width: MediaQuery.of(context).size.width*0.6, 18 | child: Padding( 19 | padding: const EdgeInsets.all(24.0), 20 | child: Column( 21 | mainAxisSize: MainAxisSize.min, 22 | children: [ 23 | Icon(EvaIcons.cloudDownloadOutline, size: 100, color: Theme.of(context).accentColor), 24 | SizedBox(height: 8), 25 | Text( 26 | Languages.of(context).labelNoMediaYet, 27 | style: TextStyle( 28 | fontWeight: FontWeight.w700, 29 | fontFamily: 'Varela' 30 | ), 31 | ), 32 | SizedBox(height: 4), 33 | Text(Languages.of(context).labelNoMediaYetJustification, 34 | textAlign: TextAlign.center), 35 | ], 36 | ), 37 | ), 38 | ), 39 | ); 40 | } 41 | } -------------------------------------------------------------------------------- /lib/ui/components/textfieldTile.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | class TextFieldTile extends StatelessWidget { 5 | final TextEditingController textController; 6 | final String labelText; 7 | final IconData icon; 8 | final TextInputType inputType; 9 | TextFieldTile({ 10 | @required this.textController, 11 | @required this.labelText, 12 | @required this.icon, 13 | @required this.inputType 14 | }); 15 | @override 16 | Widget build(BuildContext context) { 17 | return ClipRRect( 18 | borderRadius: BorderRadius.circular(100), 19 | child: TextField( 20 | keyboardType: inputType, 21 | cursorColor: Theme.of(context).accentColor, 22 | controller: textController, 23 | decoration: InputDecoration( 24 | prefixIcon: Icon(icon, 25 | color: Theme.of(context).iconTheme.color 26 | ), 27 | filled: true, 28 | fillColor: Theme.of(context).scaffoldBackgroundColor, 29 | border: UnderlineInputBorder( 30 | borderSide: BorderSide( 31 | width: 0, 32 | style: BorderStyle.none, 33 | ), 34 | ), 35 | labelText: labelText, 36 | labelStyle: TextStyle( 37 | fontWeight: FontWeight.w600, 38 | color: Theme.of(context).accentColor, 39 | ) 40 | ), 41 | style: TextStyle( 42 | color: Theme.of(context).textTheme.bodyText1.color, 43 | fontSize: 16, 44 | ), 45 | ), 46 | ); 47 | } 48 | } -------------------------------------------------------------------------------- /lib/pages/components/video/shimmer/shimmerArtworkEditor.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shimmer/shimmer.dart'; 3 | 4 | class ShimmerArtworkEditor extends StatelessWidget { 5 | const ShimmerArtworkEditor(); 6 | @override 7 | Widget build(BuildContext context) { 8 | return Container( 9 | height: 60, 10 | margin: EdgeInsets.only(left: 12, right: 12), 11 | child: Center( 12 | child: Row( 13 | children: [ 14 | Shimmer.fromColors( 15 | baseColor: Theme.of(context).cardColor.withOpacity(0.4), 16 | highlightColor: Theme.of(context).cardColor, 17 | child: Container( 18 | decoration: BoxDecoration( 19 | borderRadius: BorderRadius.circular(20), 20 | color: Theme.of(context).cardColor.withOpacity(0.4) 21 | ), 22 | width: 100, 23 | height: 50, 24 | ), 25 | ), 26 | Spacer(), 27 | Shimmer.fromColors( 28 | baseColor: Theme.of(context).cardColor.withOpacity(0.4), 29 | highlightColor: Theme.of(context).cardColor, 30 | child: Container( 31 | decoration: BoxDecoration( 32 | borderRadius: BorderRadius.circular(20), 33 | color: Theme.of(context).cardColor.withOpacity(0.4) 34 | ), 35 | width: 100, 36 | height: 50, 37 | ), 38 | ), 39 | ], 40 | ), 41 | ), 42 | ); 43 | } 44 | } -------------------------------------------------------------------------------- /lib/ui/sheets/downloadFix.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | import 'package:songtube/internal/languages.dart'; 4 | import 'package:songtube/internal/nativeMethods.dart'; 5 | import 'package:songtube/ui/components/styledBottomSheet.dart'; 6 | 7 | class DownloadFixSheet extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | return StyledBottomSheet( 11 | title: Languages.of(context).labelAndroid11Detected, 12 | content: Text( 13 | Languages.of(context).labelAndroid11DetectedJustification, 14 | style: GoogleFonts.poppins( 15 | color: Theme.of(context).textTheme.bodyText1.color, 16 | fontSize: 16, 17 | ), 18 | ), 19 | actions: [ 20 | TextButton( 21 | child: Text( 22 | "Allow", 23 | style: GoogleFonts.poppins( 24 | color: Theme.of(context).accentColor, 25 | fontSize: 18, 26 | fontWeight: FontWeight.w600, 27 | ), 28 | ), 29 | onPressed: () { 30 | NativeMethod.requestAllFilesPermission(); 31 | Navigator.pop(context); 32 | }, 33 | ), 34 | TextButton( 35 | child: Text( 36 | "Not Now", 37 | style: GoogleFonts.poppins( 38 | color: Theme.of(context).accentColor, 39 | fontSize: 18, 40 | fontWeight: FontWeight.w600, 41 | ), 42 | ), 43 | onPressed: () => Navigator.pop(context) 44 | ) 45 | ], 46 | ); 47 | } 48 | } -------------------------------------------------------------------------------- /lib/screens/homeScreen/pages/favorites.dart: -------------------------------------------------------------------------------- 1 | import 'package:eva_icons_flutter/eva_icons_flutter.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:newpipeextractor_dart/models/infoItems/video.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:songtube/provider/preferencesProvider.dart'; 6 | import 'package:songtube/ui/components/emptyIndicator.dart'; 7 | import 'package:songtube/ui/internal/snackbar.dart'; 8 | import 'package:songtube/ui/layout/streamsLargeThumbnail.dart'; 9 | 10 | class HomePageFavorites extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | PreferencesProvider prefs = Provider.of(context); 14 | return AnimatedSwitcher( 15 | duration: Duration(milliseconds: 300), 16 | child: prefs.favoriteVideos.isNotEmpty 17 | ? StreamsLargeThumbnailView( 18 | infoItems: prefs.favoriteVideos, 19 | allowSaveToFavorites: false, 20 | allowSaveToWatchLater: true, 21 | onDelete: (infoItem) { 22 | List videos = prefs.favoriteVideos; 23 | videos.removeWhere((element) => element.url == infoItem.url); 24 | prefs.favoriteVideos = videos; 25 | AppSnack.showSnackBar( 26 | icon: EvaIcons.alertCircleOutline, 27 | title: "Video removed from Favorites", 28 | context: context, 29 | ); 30 | }, 31 | ) 32 | : Container( 33 | alignment: Alignment.topCenter, 34 | child: EmptyIndicator() 35 | ) 36 | ); 37 | } 38 | } -------------------------------------------------------------------------------- /lib/screens/homeScreen/pages/watchLater.dart: -------------------------------------------------------------------------------- 1 | import 'package:eva_icons_flutter/eva_icons_flutter.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:newpipeextractor_dart/models/infoItems/video.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:songtube/provider/preferencesProvider.dart'; 6 | import 'package:songtube/ui/components/emptyIndicator.dart'; 7 | import 'package:songtube/ui/internal/snackbar.dart'; 8 | import 'package:songtube/ui/layout/streamsLargeThumbnail.dart'; 9 | 10 | class HomePageWatchLater extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | PreferencesProvider prefs = Provider.of(context); 14 | return AnimatedSwitcher( 15 | duration: Duration(milliseconds: 300), 16 | child: prefs.watchLaterVideos.isNotEmpty 17 | ? StreamsLargeThumbnailView( 18 | infoItems: prefs.watchLaterVideos, 19 | allowSaveToFavorites: true, 20 | allowSaveToWatchLater: false, 21 | onDelete: (infoItem) { 22 | List videos = prefs.watchLaterVideos; 23 | videos.removeWhere((element) => element.url == infoItem.url); 24 | prefs.watchLaterVideos = videos; 25 | AppSnack.showSnackBar( 26 | icon: EvaIcons.alertCircleOutline, 27 | title: "Video removed from Watch Later", 28 | context: context, 29 | ); 30 | }, 31 | ) 32 | : Container( 33 | alignment: Alignment.topCenter, 34 | child: EmptyIndicator() 35 | ) 36 | ); 37 | } 38 | } -------------------------------------------------------------------------------- /lib/ui/components/shimmerContainer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shimmer/shimmer.dart'; 3 | 4 | class ShimmerContainer extends StatelessWidget { 5 | final double height; 6 | final double width; 7 | final BorderRadiusGeometry borderRadius; 8 | final EdgeInsetsGeometry margin; 9 | final double aspectRatio; 10 | ShimmerContainer({ 11 | this.height, 12 | this.width, 13 | this.borderRadius, 14 | this.margin, 15 | this.aspectRatio 16 | }); 17 | @override 18 | Widget build(BuildContext context) { 19 | return Shimmer.fromColors( 20 | baseColor: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.6), 21 | highlightColor: Theme.of(context).cardColor, 22 | child: aspectRatio != null 23 | ? AspectRatio( 24 | aspectRatio: aspectRatio, 25 | child: Container( 26 | height: height, 27 | width: width, 28 | margin: margin == null ? EdgeInsets.zero : margin, 29 | decoration: BoxDecoration( 30 | borderRadius: borderRadius == null ? BorderRadius.zero : borderRadius, 31 | color: Theme.of(context).scaffoldBackgroundColor 32 | ), 33 | ), 34 | ) 35 | : Container( 36 | height: height, 37 | width: width, 38 | margin: margin == null ? EdgeInsets.zero : margin, 39 | decoration: BoxDecoration( 40 | borderRadius: borderRadius == null ? BorderRadius.zero : borderRadius, 41 | color: Theme.of(context).scaffoldBackgroundColor 42 | ), 43 | ), 44 | ); 45 | } 46 | } -------------------------------------------------------------------------------- /lib/screens/libraryScreen/components/quickAcessTile.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | // Packages 5 | import 'package:eva_icons_flutter/eva_icons_flutter.dart'; 6 | 7 | class QuickAccessTile extends StatelessWidget { 8 | final Icon tileIcon; 9 | final String title; 10 | final Function onTap; 11 | QuickAccessTile({ 12 | @required this.tileIcon, 13 | @required this.title, 14 | @required this.onTap 15 | }); 16 | @override 17 | Widget build(BuildContext context) { 18 | return GestureDetector( 19 | onTap: onTap, 20 | child: Padding( 21 | padding: EdgeInsets.only( 22 | left: 32, 23 | right: 32, 24 | top: 8, 25 | bottom: 8 26 | ), 27 | child: Container( 28 | color: Theme.of(context).scaffoldBackgroundColor, 29 | height: kToolbarHeight, 30 | child: Row( 31 | children: [ 32 | SizedBox(width: 32), 33 | tileIcon, 34 | SizedBox(width: 8), 35 | Text( 36 | title, 37 | style: TextStyle( 38 | fontSize: 15, 39 | fontFamily: "Varela", 40 | fontWeight: FontWeight.w700, 41 | color: Theme.of(context).iconTheme.color 42 | ), 43 | ), 44 | Spacer(), 45 | Container( 46 | padding: EdgeInsets.all(8), 47 | child: Icon(EvaIcons.arrowForwardOutline) 48 | ), 49 | SizedBox(width: 32) 50 | ], 51 | ), 52 | ), 53 | ), 54 | ); 55 | } 56 | } -------------------------------------------------------------------------------- /lib/internal/nativeMethods.dart: -------------------------------------------------------------------------------- 1 | // Dart 2 | import 'dart:io'; 3 | 4 | // Flutter 5 | import 'package:flutter/services.dart'; 6 | 7 | class NativeMethod { 8 | 9 | static const media = const MethodChannel("registerMedia"); 10 | static const platform = const MethodChannel("sharedTextChannel"); 11 | static const intentPlatform = const MethodChannel("intentChannel"); 12 | static const imageProcessing = const MethodChannel("imageProcessing"); 13 | 14 | // Handle Intent (Ej: when you share a YouTube link to this app) 15 | static Future handleIntent() async { 16 | String _intent = await platform.invokeMethod('getSharedText'); 17 | await platform.invokeMethod('clearSharedText'); 18 | if (_intent == null) return null; 19 | print("IntentHandler: Result: " + _intent); 20 | return _intent; 21 | } 22 | 23 | // Exit FullScreen 24 | static Future exitFullScreen() async { 25 | await platform.invokeMethod('exitFullScreen'); 26 | } 27 | 28 | // Update android MediaStore with a new File 29 | // This allows music/video players to detect the new media 30 | static void registerFile(String file) async { 31 | await media.invokeMethod('registerFile', {"file":file}); 32 | } 33 | 34 | // Open a local video with the default video player 35 | static void openVideo(String videoPath) async { 36 | if (await File(videoPath).exists()) { 37 | intentPlatform.invokeMethod('openVideo', {"videoPath": videoPath}); 38 | } 39 | } 40 | 41 | // (TEMP FIX) Request All Files Access to The App 42 | // to fix Downloads on Android 11 43 | static void requestAllFilesPermission() { 44 | intentPlatform.invokeListMethod('requestAllFilesPermission'); 45 | } 46 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | songtube 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/ui/sheets/disclaimer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | import 'package:songtube/ui/components/styledBottomSheet.dart'; 4 | import 'package:songtube/ui/dialogs/licenseDialog.dart'; 5 | 6 | class DisclaimerSheet extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | return StyledBottomSheet( 10 | title: "Disclaimer", 11 | content: Text( 12 | "This Software is released \"as-is\", without any warranty, responsibility or liability. " + 13 | "In no event shall the Author of this Software be liable for any special, consequential, " + 14 | "incidental or indirect damages whatsoever (including, without limitation, damages for " + 15 | "loss of business profits, business interruption, loss of business information, or any " + 16 | "other pecuniary loss) arising out of the use of inability to use this product, even if " + 17 | "Author of this Sotware is aware of the possibility of such damages and known defect.", 18 | style: GoogleFonts.poppins( 19 | color: Theme.of(context).textTheme.bodyText1.color, 20 | fontSize: 16, 21 | ), 22 | ), 23 | actions: [ 24 | TextButton( 25 | onPressed: () { 26 | showDialog( 27 | context: context, 28 | builder: (context) => LicenseDialog() 29 | ); 30 | }, 31 | child: Text("License", style: GoogleFonts.poppins( 32 | color: Theme.of(context).accentColor, 33 | fontWeight: FontWeight.w600, 34 | fontSize: 18 35 | )), 36 | ), 37 | ], 38 | ); 39 | } 40 | } -------------------------------------------------------------------------------- /lib/players/components/musicPlayer/ui/randomButton.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | // Packages 5 | import 'package:audio_service/audio_service.dart'; 6 | import 'package:eva_icons_flutter/eva_icons_flutter.dart'; 7 | 8 | class MusicPlayerRandomButton extends StatefulWidget { 9 | final Color iconColor; 10 | final Color enabledColor; 11 | MusicPlayerRandomButton({ 12 | @required this.iconColor, 13 | @required this.enabledColor 14 | }); 15 | @override 16 | _MusicPlayerRandomButtonState createState() => _MusicPlayerRandomButtonState(); 17 | } 18 | 19 | class _MusicPlayerRandomButtonState extends State { 20 | 21 | // Button Status 22 | bool enabled = false; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return AnimatedContainer( 27 | margin: EdgeInsets.only(right: 8), 28 | duration: Duration(milliseconds: 50), 29 | decoration: BoxDecoration( 30 | borderRadius: BorderRadius.circular(50), 31 | boxShadow: [ 32 | BoxShadow( 33 | color: enabled 34 | ? widget.enabledColor.withOpacity(0.3) 35 | : Colors.transparent, 36 | spreadRadius: 0.1, 37 | blurRadius: 15 38 | ) 39 | ] 40 | ), 41 | child: IconButton( 42 | icon: Icon( 43 | EvaIcons.shuffle2Outline, 44 | size: 16, 45 | color: enabled 46 | ? widget.enabledColor 47 | : widget.iconColor.withOpacity(0.7) 48 | ), 49 | onPressed: () async { 50 | enabled = await AudioService.customAction("enableRandom"); 51 | setState(() {}); 52 | } 53 | ), 54 | ); 55 | } 56 | } -------------------------------------------------------------------------------- /lib/players/components/musicPlayer/ui/repeatButton.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | // Packages 5 | import 'package:audio_service/audio_service.dart'; 6 | import 'package:eva_icons_flutter/eva_icons_flutter.dart'; 7 | 8 | class MusicPlayerRepeatButton extends StatefulWidget { 9 | final Color iconColor; 10 | final Color enabledColor; 11 | MusicPlayerRepeatButton({ 12 | @required this.iconColor, 13 | @required this.enabledColor 14 | }); 15 | @override 16 | _MusicPlayerRepeatButtonState createState() => _MusicPlayerRepeatButtonState(); 17 | } 18 | 19 | class _MusicPlayerRepeatButtonState extends State { 20 | 21 | // Button Status 22 | bool enabled = false; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return AnimatedContainer( 27 | duration: Duration(milliseconds: 50), 28 | margin: EdgeInsets.only(left: 8), 29 | decoration: BoxDecoration( 30 | borderRadius: BorderRadius.circular(50), 31 | boxShadow: [ 32 | BoxShadow( 33 | color: enabled 34 | ? widget.enabledColor.withOpacity(0.3) 35 | : Colors.transparent, 36 | spreadRadius: 0.1, 37 | blurRadius: 15 38 | ) 39 | ] 40 | ), 41 | child: IconButton( 42 | icon: Icon( 43 | EvaIcons.repeatOutline, 44 | size: 16, 45 | color: enabled 46 | ? widget.enabledColor 47 | : widget.iconColor.withOpacity(0.7) 48 | ), 49 | onPressed: () async { 50 | enabled = await AudioService.customAction("enableRepeat"); 51 | setState(() {}); 52 | } 53 | ), 54 | ); 55 | } 56 | } -------------------------------------------------------------------------------- /lib/internal/lyricsProviders.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:http/http.dart'; 4 | 5 | class LyricsProviders { 6 | 7 | static Future lyricsOvh({String author, String title}) async { 8 | Client client = Client(); 9 | Response response; 10 | try { 11 | response = await client.get(Uri.parse( 12 | "https://api.lyrics.ovh/v1/" 13 | "${author.replaceAll('&', '')}/" 14 | "${title.replaceAll('&', '')}") 15 | ).timeout(Duration(seconds: 5)); 16 | } catch (_) { 17 | return ""; 18 | } 19 | client.close(); 20 | if ((jsonDecode(response.body) as Map).containsKey('error')) { 21 | return ""; 22 | } else { 23 | return jsonDecode(response.body)["lyrics"]; 24 | } 25 | } 26 | 27 | static final happiDevKey = "e1de5fbTOztuNxXBGZ1m39MbY0SPfUUQQm2pbLSdEADsMMm1duk4xQBa"; 28 | 29 | static Future lyricsHappiDev({String title}) async { 30 | Client client = Client(); 31 | var response = await client.get(Uri.parse( 32 | "https://api.happi.dev/v1/music?q=${title.replaceAll('&', '')}" 33 | "&limit=1&apikey=$happiDevKey&type=track") 34 | ); 35 | var responseJson = jsonDecode(response.body); 36 | if (responseJson["success"] == true) { 37 | var lyricsResponse = await client.get( 38 | responseJson["result"][0]["api_lyrics"] + 39 | "?apikey=$happiDevKey" 40 | ); 41 | var lyricsJson = jsonDecode(lyricsResponse.body); 42 | if (lyricsJson["success"] == true) { 43 | return lyricsJson["result"]["lyrics"]; 44 | } else { 45 | client.close(); 46 | return ""; 47 | } 48 | } else { 49 | client.close(); 50 | return ""; 51 | } 52 | } 53 | 54 | 55 | 56 | } -------------------------------------------------------------------------------- /lib/ui/animations/FadeIn.dart: -------------------------------------------------------------------------------- 1 | // Dart 2 | import 'dart:async'; 3 | 4 | // Flutter 5 | import 'package:flutter/material.dart'; 6 | 7 | class FadeInTransition extends StatefulWidget { 8 | /// [child] to be Animated 9 | final Widget child; 10 | /// Animation Duration, default is 200 Milliseconds 11 | final Duration duration; 12 | /// Animation Curve, default is Linear 13 | final Curve curve; 14 | /// Delay before starting Animation 15 | final Duration delay; 16 | 17 | FadeInTransition({ 18 | @required this.child, 19 | this.duration, 20 | this.curve, 21 | this.delay 22 | }); 23 | 24 | @override 25 | _FadeInTransitionState createState() => _FadeInTransitionState(); 26 | } 27 | 28 | class _FadeInTransitionState extends State with TickerProviderStateMixin { 29 | AnimationController _animController; 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | _animController = 35 | AnimationController(vsync: this, duration: widget.duration == null 36 | ? Duration(milliseconds: 200) 37 | : widget.duration 38 | ); 39 | if (widget.delay == null) { 40 | _animController.forward(); 41 | } else { 42 | Timer(widget.delay, () { 43 | _animController.forward(); 44 | }); 45 | } 46 | } 47 | 48 | @override 49 | void dispose() { 50 | _animController.dispose(); 51 | super.dispose(); 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return FadeTransition( 57 | opacity: new CurvedAnimation( 58 | parent: _animController, 59 | curve: widget.curve == null 60 | ? Curves.linear 61 | : widget.curve 62 | ), 63 | child: widget.child, 64 | ); 65 | } 66 | } -------------------------------------------------------------------------------- /lib/internal/models/songFile.dart: -------------------------------------------------------------------------------- 1 | // Dart 2 | import 'dart:core'; 3 | 4 | // Flutter 5 | import 'package:flutter/material.dart'; 6 | 7 | class SongFile { 8 | 9 | String id; 10 | String title; 11 | String album; 12 | String author; 13 | String duration; 14 | String downloadType; 15 | String path; 16 | String fileSize; 17 | String coverUrl; 18 | String coverPath; 19 | 20 | SongFile({ 21 | @required this.id, 22 | @required this.title, 23 | @required this.album, 24 | @required this.author, 25 | @required this.duration, 26 | @required this.downloadType, 27 | @required this.path, 28 | @required this.fileSize, 29 | @required this.coverUrl, 30 | this.coverPath, 31 | }); 32 | 33 | SongFile.toDatabase({ 34 | @required this.title, 35 | @required this.album, 36 | @required this.author, 37 | @required this.duration, 38 | @required this.downloadType, 39 | @required this.path, 40 | @required this.fileSize, 41 | @required this.coverUrl, 42 | }); 43 | 44 | SongFile.fromMap(Map map) { 45 | id = map["id"].toString(); 46 | title = map["title"]; 47 | album = map["album"]; 48 | author = map["author"]; 49 | duration = map["duration"].toString(); 50 | downloadType = map["downloadType"]; 51 | path = map["path"]; 52 | fileSize = map["fileSize"].toString(); 53 | coverUrl = map["coverUrl"]; 54 | } 55 | 56 | Map toMap() { 57 | return { 58 | "title": this.title, 59 | "album": this.album, 60 | "author": this.author, 61 | "duration": this.duration.toString(), 62 | "downloadType": this.downloadType, 63 | "path": this.path, 64 | "fileSize": this.fileSize, 65 | "coverUrl": this.coverUrl, 66 | }; 67 | } 68 | } -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class org.schabi.newpipe.extractor.** { *; } 2 | -keep class org.ocpsoft.prettytime.i18n.** { *; } 3 | 4 | -keep class org.mozilla.javascript.** { *; } 5 | 6 | -keep class org.mozilla.classfile.ClassFileWriter 7 | -keep class com.google.android.exoplayer2.** { *; } 8 | 9 | -dontwarn org.mozilla.javascript.tools.** 10 | -dontwarn android.arch.util.paging.CountedDataSource 11 | -dontwarn android.arch.persistence.room.paging.LimitOffsetDataSource 12 | -keep class com.artxdev.** { *; } 13 | -keep class androidx.lifecycle.** { *; } 14 | -keep class org.jaudiotagger.** { *; } 15 | -keep class com.example.audio_tagger.** { *; } 16 | -dontobfuscate 17 | 18 | ## Flutter wrapper 19 | -keep class io.flutter.app.** { *; } 20 | -keep class io.flutter.plugin.** { *; } 21 | -keep class io.flutter.util.** { *; } 22 | -keep class io.flutter.view.** { *; } 23 | -keep class io.flutter.** { *; } 24 | -keep class io.flutter.plugins.** { *; } 25 | 26 | # -keep class com.google.firebase.** { *; } // uncomment this if you are using firebase in the project 27 | 28 | -keep class com.arthenica.mobileffmpeg.Config { 29 | native ; 30 | void log(long, int, byte[]); 31 | void statistics(long, int, float, float, long , int, double, double); 32 | } 33 | 34 | -keep class com.arthenica.mobileffmpeg.AbiDetect { 35 | native ; 36 | } 37 | 38 | # Rules for OkHttp. Copy paste from https://github.com/square/okhttp 39 | -dontwarn okhttp3.** 40 | -dontwarn okio.** 41 | -dontwarn javax.annotation.** 42 | # A resource is loaded with a relative path so the package of this class must be preserved. 43 | -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase 44 | -keepclassmembers class * implements java.io.Serializable { 45 | static final long serialVersionUID; 46 | !static !transient ; 47 | private void writeObject(java.io.ObjectOutputStream); 48 | private void readObject(java.io.ObjectInputStream); 49 | } -------------------------------------------------------------------------------- /lib/ui/internal/popupMenu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FlexiblePopupItem { 4 | 5 | String title; 6 | String value; 7 | 8 | FlexiblePopupItem({ 9 | this.title, 10 | this.value 11 | }); 12 | 13 | } 14 | 15 | class FlexiblePopupMenu extends StatelessWidget { 16 | final Widget child; 17 | final List items; 18 | final Function(String) onItemTap; 19 | final double borderRadius; 20 | final EdgeInsetsGeometry padding; 21 | FlexiblePopupMenu({ 22 | @required this.child, 23 | @required this.items, 24 | @required this.onItemTap, 25 | this.borderRadius, 26 | this.padding 27 | }); 28 | @override 29 | Widget build(BuildContext context) { 30 | return GestureDetector( 31 | onTapDown: (details) { 32 | showMenu( 33 | color: Theme.of(context).popupMenuTheme.color, 34 | shape: RoundedRectangleBorder( 35 | borderRadius: borderRadius == null 36 | ? BorderRadius.zero 37 | : BorderRadius.circular(borderRadius), 38 | ), 39 | context: context, 40 | position: RelativeRect.fromLTRB( 41 | details.globalPosition.dx, 42 | details.globalPosition.dy, 43 | 0, 0 44 | ), 45 | items: items.map((e) { 46 | return PopupMenuItem( 47 | child: Text( 48 | e.title, style: TextStyle( 49 | color: Theme.of(context) 50 | .textTheme.bodyText1.color, 51 | fontSize: 14 52 | ), 53 | ), 54 | value: "${e.value}", 55 | ); 56 | }).toList() 57 | ).then((value) { 58 | onItemTap(value); 59 | }); 60 | }, 61 | child: Padding( 62 | padding: padding == null ? EdgeInsets.zero : padding, 63 | child: child 64 | ), 65 | ); 66 | } 67 | } -------------------------------------------------------------------------------- /lib/players/components/musicPlayer/ui/marqueeWidget.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | class MarqueeWidget extends StatefulWidget { 5 | final Widget child; 6 | final Axis direction; 7 | final Duration animationDuration, backDuration, pauseDuration; 8 | 9 | MarqueeWidget({ 10 | @required this.child, 11 | this.direction: Axis.horizontal, 12 | this.animationDuration: const Duration(milliseconds: 3000), 13 | this.backDuration: const Duration(milliseconds: 800), 14 | this.pauseDuration: const Duration(milliseconds: 800), 15 | }); 16 | 17 | @override 18 | _MarqueeWidgetState createState() => _MarqueeWidgetState(); 19 | } 20 | 21 | class _MarqueeWidgetState extends State { 22 | ScrollController scrollController; 23 | 24 | @override 25 | void initState() { 26 | scrollController = ScrollController(initialScrollOffset: 0); 27 | WidgetsBinding.instance.addPostFrameCallback(scroll); 28 | super.initState(); 29 | } 30 | 31 | @override 32 | void dispose(){ 33 | scrollController.dispose(); 34 | super.dispose(); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return SingleChildScrollView( 40 | child: widget.child, 41 | scrollDirection: widget.direction, 42 | controller: scrollController, 43 | ); 44 | } 45 | 46 | void scroll(_) async { 47 | while (scrollController.hasClients) { 48 | await Future.delayed(widget.pauseDuration); 49 | if(scrollController.hasClients) 50 | await scrollController.animateTo( 51 | scrollController.position.maxScrollExtent, 52 | duration: widget.animationDuration, 53 | curve: Curves.ease); 54 | await Future.delayed(widget.pauseDuration); 55 | if(scrollController.hasClients) 56 | await scrollController.animateTo(0.0, 57 | duration: widget.backDuration, curve: Curves.easeOut); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /lib/internal/models/subscription.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; 4 | 5 | class ChannelSubscription { 6 | 7 | String url; 8 | String id; 9 | String name; 10 | String avatarUrl; 11 | DateTime date; 12 | bool enableNotifications; 13 | 14 | ChannelSubscription( 15 | this.url, 16 | this.id, 17 | this.name, 18 | this.avatarUrl, 19 | this.date, 20 | this.enableNotifications 21 | ); 22 | 23 | static ChannelSubscription generateFromChannel(YoutubeChannel channel) { 24 | return ChannelSubscription( 25 | channel.url, 26 | channel.id, 27 | channel.name, 28 | channel.avatarUrl, 29 | DateTime.now(), 30 | false 31 | ); 32 | } 33 | 34 | Map toMap() { 35 | return { 36 | 'url': url, 37 | 'id': id, 38 | 'name': name, 39 | 'avatarUrl': avatarUrl, 40 | 'date': date.toString(), 41 | 'enableNotifications': enableNotifications.toString() 42 | }; 43 | } 44 | 45 | static ChannelSubscription fromMap(map) { 46 | return ChannelSubscription( 47 | map['url'], 48 | map['id'], 49 | map['name'], 50 | map['avatarUrl'], 51 | DateTime.parse(map['date']), 52 | map['enableNotifications'] == "true" ? true : false 53 | ); 54 | } 55 | 56 | static String toJsonList(List channels) { 57 | if (channels == null || channels.isEmpty) return ""; 58 | return jsonEncode(List.generate(channels.length, (index) { 59 | return channels[index].toMap(); 60 | }).toList()); 61 | } 62 | 63 | static List fromJsonList(String json) { 64 | if (json == null || json == "") return []; 65 | var channelsMap = jsonDecode(json); 66 | return channelsMap.isNotEmpty 67 | ? List.generate(channelsMap.length, (index) { 68 | return ChannelSubscription.fromMap(channelsMap[index]); 69 | }).toList() 70 | : []; 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /lib/screens/musicScreen/components/mediaListBase.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | import 'package:permission_handler/permission_handler.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:songtube/provider/mediaProvider.dart'; 6 | import 'package:songtube/screens/musicScreen/components/downloadsEmpty.dart'; 7 | import 'package:songtube/screens/musicScreen/components/loadingListWidget.dart'; 8 | import 'package:songtube/screens/musicScreen/components/noPermissionWidget.dart'; 9 | import 'package:songtube/screens/musicScreen/components/playlistEmpty.dart'; 10 | 11 | enum MediaListBaseType { Downloads, Any } 12 | 13 | class MediaListBase extends StatelessWidget { 14 | final Widget child; 15 | final bool isLoading; 16 | final bool isEmpty; 17 | final MediaListBaseType listType; 18 | MediaListBase({ 19 | @required this.child, 20 | @required this.isLoading, 21 | @required this.isEmpty, 22 | @required this.listType, 23 | }); 24 | @override 25 | Widget build(BuildContext context) { 26 | MediaProvider mediaProvider = Provider.of(context); 27 | return AnimatedSwitcher( 28 | duration: Duration(milliseconds: 200), 29 | child: animatedSwitcherChild(mediaProvider), 30 | ); 31 | } 32 | 33 | Widget animatedSwitcherChild(mediaProvider) { 34 | if (!mediaProvider.storagePermission) { 35 | return NoPermissionWidget( 36 | onPermissionRequest: () { 37 | Permission.storage.request().then((value) { 38 | if (value == PermissionStatus.granted) { 39 | mediaProvider.storagePermission = true; 40 | mediaProvider.loadSongList(); 41 | } 42 | }); 43 | } 44 | ); 45 | } else if (!isEmpty) { 46 | return child; 47 | } else if (isLoading) { 48 | return MediaLoadingWidget(); 49 | } else { 50 | if (listType == MediaListBaseType.Downloads) { 51 | return MediaDownloadsEmpty(); 52 | } else { 53 | return PlaylistEmptyWidget(); 54 | } 55 | } 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /lib/ui/components/navigationBar.dart: -------------------------------------------------------------------------------- 1 | import 'package:eva_icons_flutter/eva_icons_flutter.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; 4 | import 'package:songtube/internal/languages.dart'; 5 | 6 | class AppBottomNavigationBar extends StatelessWidget { 7 | final int currentIndex; 8 | final Function(int) onItemTap; 9 | AppBottomNavigationBar({ 10 | @required this.currentIndex, 11 | @required this.onItemTap, 12 | }); 13 | @override 14 | Widget build(BuildContext context) { 15 | return BottomNavigationBar( 16 | backgroundColor: Theme.of(context).cardColor, 17 | currentIndex: currentIndex, 18 | selectedLabelStyle: TextStyle( 19 | fontFamily: 'Product Sans', 20 | fontWeight: FontWeight.w600, 21 | letterSpacing: 0.2 22 | ), 23 | unselectedLabelStyle: TextStyle( 24 | fontFamily: 'Product Sans', 25 | fontWeight: FontWeight.w600, 26 | letterSpacing: 0.2 27 | ), 28 | iconSize: 22, 29 | selectedFontSize: 12, 30 | unselectedFontSize: 12, 31 | elevation: 8, 32 | selectedItemColor: Theme.of(context).accentColor, 33 | unselectedItemColor: Theme.of(context).iconTheme.color, 34 | type: BottomNavigationBarType.fixed, 35 | onTap: (int index) => onItemTap(index), 36 | items: [ 37 | BottomNavigationBarItem( 38 | icon: Icon(EvaIcons.homeOutline), 39 | label: Languages.of(context).labelHome 40 | ), 41 | BottomNavigationBarItem( 42 | icon: Icon(EvaIcons.bookOpenOutline), 43 | label: "Channels" 44 | ), 45 | BottomNavigationBarItem( 46 | icon: Icon(EvaIcons.cloudDownloadOutline), 47 | label: Languages.of(context).labelDownloads 48 | ), 49 | BottomNavigationBarItem( 50 | icon: Icon(EvaIcons.musicOutline), 51 | label: Languages.of(context).labelMusic 52 | ), 53 | BottomNavigationBarItem( 54 | icon: Icon(MdiIcons.folderOutline), 55 | label: Languages.of(context).labelLibrary 56 | ) 57 | ] 58 | ); 59 | } 60 | } -------------------------------------------------------------------------------- /lib/ui/sheets/searchFilters.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:newpipeextractor_dart/models/filters.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:songtube/provider/managerProvider.dart'; 5 | import 'package:songtube/ui/components/styledBottomSheet.dart'; 6 | 7 | class SearchFiltersSheet extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | ManagerProvider manager = Provider.of(context); 11 | return StyledBottomSheet( 12 | title: "Search Filters", 13 | contentPadding: EdgeInsets.all(12), 14 | content: ListView.builder( 15 | shrinkWrap: true, 16 | itemCount: YoutubeSearchFilter.searchFilters.length, 17 | itemBuilder: (context, index) { 18 | String filter = YoutubeSearchFilter.searchFilters[index]; 19 | return CheckboxListTile( 20 | title: Text( 21 | (filter[0].toUpperCase() + filter.substring(1)) 22 | .replaceAll("_", " "), 23 | style: TextStyle( 24 | color: Theme.of(context).textTheme.bodyText1.color, 25 | fontSize: 16, 26 | fontFamily: 'Product Sans', 27 | fontWeight: FontWeight.w600 28 | ), 29 | ), 30 | value: manager.searchFilters.contains(filter), 31 | onChanged: (_) { 32 | if (manager.searchFilters.contains(filter)) { 33 | manager.searchFilters.removeWhere((element) => element == filter); 34 | } else { 35 | manager.searchFilters.add(filter); 36 | } 37 | manager.setState(); 38 | } 39 | ); 40 | }, 41 | ), 42 | actions: [ 43 | TextButton( 44 | child: Text( 45 | "Close", 46 | style: TextStyle( 47 | color: Theme.of(context).accentColor, 48 | fontFamily: 'Product Sans', 49 | fontWeight: FontWeight.w700, 50 | fontSize: 18 51 | ), 52 | ), 53 | onPressed: () => Navigator.pop(context) 54 | ) 55 | ], 56 | ); 57 | } 58 | } -------------------------------------------------------------------------------- /lib/ui/sheets/joinTelegram.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:songtube/internal/languages.dart'; 5 | import 'package:songtube/provider/preferencesProvider.dart'; 6 | import 'package:songtube/ui/components/styledBottomSheet.dart'; 7 | import 'package:url_launcher/url_launcher.dart'; 8 | 9 | class JoinTelegramSheet extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | PreferencesProvider prefs = Provider.of(context, listen: false); 13 | return StyledBottomSheet( 14 | actionsPadding: EdgeInsets.only( 15 | right: 24, left: 24, bottom: 12 16 | ), 17 | addBottomPadding: true, 18 | leading: Icon(MdiIcons.telegram, color: Colors.blue), 19 | title: Languages.of(context).labelJoinTelegramChannel, 20 | content: Text( 21 | Languages.of(context).labelJoinTelegramJustification, 22 | style: TextStyle( 23 | color: Theme.of(context).textTheme.bodyText1.color, 24 | fontSize: 16 25 | ), 26 | ), 27 | actions: [ 28 | TextButton( 29 | child: Text(Languages.of(context).labelJoin, 30 | style: TextStyle( 31 | fontWeight: FontWeight.w600 32 | )), 33 | onPressed: () { 34 | prefs.showJoinTelegramDialog = false; 35 | launch("https://t.me/songtubechannel"); 36 | Navigator.pop(context); 37 | }, 38 | ), 39 | TextButton( 40 | child: Text(Languages.of(context).labelRemindLater, 41 | style: TextStyle( 42 | fontWeight: FontWeight.w600 43 | )), 44 | onPressed: () { 45 | prefs.remindTelegramLater = true; 46 | Navigator.pop(context); 47 | }, 48 | ), 49 | TextButton( 50 | child: Text(Languages.of(context).labelNo, 51 | style: TextStyle( 52 | fontWeight: FontWeight.w600 53 | )), 54 | onPressed: () { 55 | prefs.showJoinTelegramDialog = false; 56 | Navigator.pop(context); 57 | }, 58 | ) 59 | ], 60 | ); 61 | } 62 | } -------------------------------------------------------------------------------- /lib/screens/musicScreen/components/noPermissionWidget.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | // Packages 5 | import 'package:eva_icons_flutter/eva_icons_flutter.dart'; 6 | import 'package:songtube/internal/languages.dart'; 7 | 8 | class NoPermissionWidget extends StatelessWidget { 9 | final Function onPermissionRequest; 10 | NoPermissionWidget({ 11 | @required this.onPermissionRequest 12 | }); 13 | @override 14 | Widget build(BuildContext context) { 15 | return Center( 16 | child: Column( 17 | mainAxisAlignment: MainAxisAlignment.center, 18 | children: [ 19 | Icon(EvaIcons.saveOutline, size: 80), 20 | Container( 21 | margin: EdgeInsets.only(top: 16, bottom: 16), 22 | child: Text( 23 | Languages.of(context).labelNoPermissionJustification, 24 | style: TextStyle( 25 | fontFamily: 'YTSans', 26 | fontSize: 20 27 | ), 28 | textAlign: TextAlign.center, 29 | ), 30 | ), 31 | GestureDetector( 32 | onTap: () => onPermissionRequest(), 33 | child: Container( 34 | margin: EdgeInsets.only(bottom: 32), 35 | height: 50, 36 | decoration: BoxDecoration( 37 | borderRadius: BorderRadius.circular(10), 38 | color: Theme.of(context).accentColor 39 | ), 40 | child: Row( 41 | mainAxisSize: MainAxisSize.min, 42 | children: [ 43 | Container( 44 | margin: EdgeInsets.only(left: 16, right: 8), 45 | child: Text( 46 | Languages.of(context).labelAllow + " " + 47 | Languages.of(context).labelAccess, 48 | style: TextStyle( 49 | fontSize: 16, 50 | color: Colors.white, 51 | fontWeight: FontWeight.w600 52 | ) 53 | ), 54 | ), 55 | Container( 56 | margin: EdgeInsets.only(right: 8), 57 | child: Icon( 58 | EvaIcons.radioButtonOnOutline, 59 | color: Colors.white, 60 | ) 61 | ) 62 | ], 63 | ), 64 | ), 65 | ), 66 | ], 67 | ), 68 | ); 69 | } 70 | } -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/internal/systemUi.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:songtube/provider/configurationProvider.dart'; 5 | import 'package:songtube/provider/mediaProvider.dart'; 6 | import 'package:songtube/provider/preferencesProvider.dart'; 7 | 8 | void setSystemUiColor(BuildContext context) { 9 | ConfigurationProvider config = Provider.of(context, listen: false); 10 | MediaProvider mediaProvider = Provider.of(context, listen: false); 11 | PreferencesProvider prefs = Provider.of(context, listen: false); 12 | Brightness _systemBrightness = Theme.of(context).brightness; 13 | Brightness _statusBarBrightness = _systemBrightness == Brightness.light 14 | ? Brightness.dark 15 | : Brightness.light; 16 | if (!mediaProvider.fwController.isAttached) { 17 | SystemChrome.setSystemUIOverlayStyle( 18 | SystemUiOverlayStyle( 19 | statusBarColor: Colors.transparent, 20 | statusBarBrightness: _statusBarBrightness, 21 | statusBarIconBrightness: _statusBarBrightness, 22 | systemNavigationBarColor: Theme.of(context).cardColor, 23 | systemNavigationBarIconBrightness: _statusBarBrightness, 24 | ), 25 | ); 26 | } else { 27 | double position = mediaProvider.fwController.panelPosition; 28 | int sdkInt = config.preferences.sdkInt; 29 | if (position > 0.95) { 30 | bool mediaBlurBackground = prefs.enablePlayerBlurBackground; 31 | SystemChrome.setSystemUIOverlayStyle( 32 | SystemUiOverlayStyle( 33 | statusBarIconBrightness: mediaBlurBackground ? mediaProvider.textColor == Colors.black 34 | ? Brightness.dark : Brightness.light : _statusBarBrightness, 35 | systemNavigationBarIconBrightness: mediaBlurBackground ? sdkInt >= 30 ? mediaProvider.textColor == Colors.black 36 | ? Brightness.dark : Brightness.light : null : _statusBarBrightness, 37 | ), 38 | ); 39 | } else if (position < 0.95) { 40 | SystemChrome.setSystemUIOverlayStyle( 41 | SystemUiOverlayStyle( 42 | statusBarColor: Colors.transparent, 43 | statusBarBrightness: _statusBarBrightness, 44 | statusBarIconBrightness: _statusBarBrightness, 45 | systemNavigationBarColor: Theme.of(context).cardColor, 46 | systemNavigationBarIconBrightness: _statusBarBrightness, 47 | ), 48 | ); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /lib/screens/downloadScreen/tabs/cancelledTab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:permission_handler/permission_handler.dart'; 3 | import 'package:autolist/autolist.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:songtube/internal/download/downloadSet.dart'; 6 | import 'package:songtube/provider/downloadsProvider.dart'; 7 | import 'package:songtube/screens/downloadScreen/components/downloadTile.dart'; 8 | import 'package:songtube/ui/components/emptyIndicator.dart'; 9 | 10 | class DownloadsCancelledTab extends StatefulWidget { 11 | @override 12 | _DownloadsCancelledTabState createState() => _DownloadsCancelledTabState(); 13 | } 14 | 15 | class _DownloadsCancelledTabState extends State { 16 | @override 17 | Widget build(BuildContext context) { 18 | return AnimatedSwitcher( 19 | duration: Duration(milliseconds: 200), 20 | child: downloadsBody(context) 21 | ); 22 | } 23 | 24 | Widget downloadsBody(BuildContext context) { 25 | DownloadsProvider downloadsProvider = Provider.of(context); 26 | if (downloadsProvider.cancelledList.isNotEmpty) { 27 | return Padding( 28 | padding: EdgeInsets.only(top: 8), 29 | child: AutoList( 30 | items: downloadsProvider.cancelledList, 31 | duration: Duration(milliseconds: 400), 32 | itemBuilder: (context, infoset) { 33 | return Padding( 34 | padding: EdgeInsets.only(left: 16, right: 16, bottom: 8), 35 | child: DownloadTile( 36 | dataProgress: infoset.dataProgress.stream, 37 | progressBar: infoset.progressBar.stream, 38 | currentAction: infoset.currentAction.stream, 39 | metadata: infoset.downloadItem.tags, 40 | downloadType: infoset.downloadItem.downloadType, 41 | errorReason: infoset.errorReason, 42 | onDownloadCancel: () { 43 | Permission.storage.request().then((value) { 44 | if (value == PermissionStatus.granted) { 45 | downloadsProvider.retryDownload(infoset.downloadId); 46 | setState(() {}); 47 | } 48 | }); 49 | }, 50 | cancelDownloadIcon: Icon(Icons.refresh, size: 18) 51 | ) 52 | ); 53 | }, 54 | ), 55 | ); 56 | } else { 57 | return Align( 58 | alignment: Alignment.topCenter, 59 | child: const EmptyIndicator() 60 | ); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /lib/pages/watchHistory.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:newpipeextractor_dart/models/infoItems/video.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:songtube/provider/preferencesProvider.dart'; 5 | import 'package:songtube/provider/videoPageProvider.dart'; 6 | import 'package:songtube/ui/layout/streamsListTile.dart'; 7 | 8 | class WatchHistoryPage extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | PreferencesProvider prefs = Provider.of(context); 12 | VideoPageProvider pageProvider = Provider.of(context); 13 | List history = prefs.watchHistory; 14 | return Scaffold( 15 | backgroundColor: Theme.of(context).cardColor, 16 | appBar: AppBar( 17 | title: Text( 18 | "Watch History", 19 | style: TextStyle( 20 | fontFamily: 'Product Sans', 21 | fontWeight: FontWeight.w600, 22 | fontSize: 24, 23 | color: Theme.of(context).textTheme.bodyText1.color 24 | ), 25 | ), 26 | elevation: 0, 27 | backgroundColor: Theme.of(context).cardColor, 28 | leading: IconButton( 29 | icon: Icon(Icons.arrow_back_ios_new_rounded, color: Theme.of(context).iconTheme.color), 30 | onPressed: () { 31 | Navigator.pop(context); 32 | }, 33 | ), 34 | ), 35 | body: Column( 36 | children: [ 37 | Divider( 38 | height: 1, 39 | thickness: 1, 40 | color: Colors.grey[600].withOpacity(0.1), 41 | indent: 12, 42 | endIndent: 12 43 | ), 44 | Expanded( 45 | child: history.isNotEmpty 46 | ? StreamsListTileView( 47 | streams: history, 48 | onTap: (stream, index) { 49 | Navigator.of(context).pop(); 50 | pageProvider.infoItem = stream; 51 | }, 52 | onDelete: (item) => prefs.deleteFromWatchHistory(item as StreamInfoItem) 53 | ) 54 | : Center( 55 | child: Text( 56 | "History is Empty!", 57 | style: TextStyle( 58 | fontWeight: FontWeight.w600, 59 | fontSize: 16 60 | ), 61 | ), 62 | ), 63 | ), 64 | Container( 65 | height: MediaQuery.of(context).padding.bottom, 66 | color: Theme.of(context).cardColor 67 | ) 68 | ], 69 | ) 70 | ); 71 | } 72 | } -------------------------------------------------------------------------------- /lib/players/components/videoPlayer/progressBar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:video_player/video_player.dart'; 3 | 4 | class VideoPlayerProgressBar extends StatelessWidget { 5 | final VideoPlayerController controller; 6 | final Stream position; 7 | VideoPlayerProgressBar({ 8 | @required this.controller, 9 | @required this.position 10 | }); 11 | @override 12 | Widget build(BuildContext context) { 13 | return Container( 14 | margin: EdgeInsets.all(8), 15 | child: Row( 16 | mainAxisAlignment: MainAxisAlignment.center, 17 | crossAxisAlignment: CrossAxisAlignment.center, 18 | children: [ 19 | StreamBuilder( 20 | stream: position, 21 | builder: (context, snapshot) { 22 | if (snapshot.hasData) { 23 | return Padding( 24 | padding: EdgeInsets.only(left: 16, right: 8, top: 4), 25 | child: Text( 26 | "${snapshot.data.inMinutes.toString().padLeft(2, '0')}:" + 27 | "${snapshot.data.inSeconds.remainder(60).toString().padLeft(2, '0')}", 28 | style: TextStyle( 29 | fontSize: 12, 30 | color: Colors.white 31 | ), 32 | ) 33 | ); 34 | } else { 35 | return Padding( 36 | padding: EdgeInsets.only(left: 16, right: 8, top: 4), 37 | child: Text( 38 | "00:00", 39 | style: TextStyle( 40 | fontSize: 12, 41 | color: Colors.white 42 | ), 43 | ) 44 | ); 45 | } 46 | } 47 | ), 48 | Expanded( 49 | child: VideoProgressIndicator( 50 | controller, 51 | allowScrubbing: true, 52 | colors: VideoProgressColors( 53 | playedColor: Theme.of(context).accentColor, 54 | bufferedColor: Colors.grey[500].withOpacity(0.6), 55 | backgroundColor: Colors.grey[600].withOpacity(0.6) 56 | ), 57 | ), 58 | ), 59 | Padding( 60 | padding: EdgeInsets.only(left: 8, right: 16, top: 4), 61 | child: Text( 62 | "${controller.value.duration.inMinutes.toString().padLeft(2, '0')}:" + 63 | "${controller.value.duration.inSeconds.remainder(60).toString().padLeft(2, '0')}", 64 | style: TextStyle( 65 | fontSize: 12, 66 | color: Colors.white 67 | ), 68 | ) 69 | ) 70 | ], 71 | ), 72 | ); 73 | } 74 | } -------------------------------------------------------------------------------- /lib/screens/downloadScreen/tabs/queueTab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:songtube/internal/download/downloadSet.dart'; 4 | import 'package:songtube/provider/downloadsProvider.dart'; 5 | import 'package:songtube/screens/downloadScreen/components/downloadTile.dart'; 6 | import 'package:songtube/ui/components/emptyIndicator.dart'; 7 | import 'package:autolist/autolist.dart'; 8 | 9 | class DownloadsQueueTab extends StatefulWidget { 10 | @override 11 | _DownloadsQueueTabState createState() => _DownloadsQueueTabState(); 12 | } 13 | 14 | class _DownloadsQueueTabState extends State { 15 | @override 16 | Widget build(BuildContext context) { 17 | return AnimatedSwitcher( 18 | duration: Duration(milliseconds: 200), 19 | child: downloadsBody(context) 20 | ); 21 | } 22 | 23 | Widget downloadsBody(BuildContext context) { 24 | DownloadsProvider downloadsProvider = Provider.of(context); 25 | if (downloadsProvider.downloadingList.isNotEmpty) { 26 | return AnimatedSwitcher( 27 | duration: Duration(milliseconds: 400), 28 | child: downloadsProvider.downloadingList.isNotEmpty ? Align( 29 | alignment: Alignment.topCenter, 30 | child: AutoList( 31 | shrinkWrap: true, 32 | physics: NeverScrollableScrollPhysics(), 33 | items: downloadsProvider.downloadingList, 34 | duration: Duration(milliseconds: 400), 35 | itemBuilder: (context, infoset) { 36 | return StreamBuilder( 37 | stream: infoset.downloadStatusStream.stream, 38 | builder: (context, snapshot) { 39 | return DownloadTile( 40 | dataProgress: infoset.dataProgress.stream, 41 | progressBar: infoset.progressBar.stream, 42 | currentAction: infoset.currentAction.stream, 43 | metadata: infoset.downloadItem.tags, 44 | downloadType: infoset.downloadItem.downloadType, 45 | onDownloadCancel: snapshot.data == DownloadStatus.Downloading 46 | ? () { 47 | infoset.cancelDownload = true; 48 | } : null, 49 | cancelDownloadIcon: snapshot.data == DownloadStatus.Downloading 50 | ? Icon(Icons.clear, size: 18) 51 | : Container() 52 | ); 53 | } 54 | ); 55 | }, 56 | ), 57 | ) : Container(), 58 | ); 59 | } else { 60 | return Align( 61 | alignment: Alignment.topCenter, 62 | child: const EmptyIndicator() 63 | ); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /lib/pages/components/video/shimmer/shimmerVideoEngagement.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shimmer/shimmer.dart'; 3 | 4 | class ShimmerVideoEngagement extends StatelessWidget { 5 | const ShimmerVideoEngagement(); 6 | @override 7 | Widget build(BuildContext context) { 8 | return Row( 9 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 10 | crossAxisAlignment: CrossAxisAlignment.center, 11 | children: [ 12 | Shimmer.fromColors( 13 | baseColor: Theme.of(context).cardColor.withOpacity(0.4), 14 | highlightColor: Theme.of(context).cardColor, 15 | child: Container( 16 | width: 65, 17 | height: 65, 18 | decoration: BoxDecoration( 19 | borderRadius: BorderRadius.circular(50), 20 | color: Theme.of(context).cardColor.withOpacity(0.4) 21 | ), 22 | ), 23 | ), 24 | Shimmer.fromColors( 25 | baseColor: Theme.of(context).cardColor.withOpacity(0.4), 26 | highlightColor: Theme.of(context).cardColor, 27 | child: Container( 28 | width: 65, 29 | height: 65, 30 | decoration: BoxDecoration( 31 | borderRadius: BorderRadius.circular(50), 32 | color: Theme.of(context).cardColor.withOpacity(0.4) 33 | ), 34 | ), 35 | ), 36 | Shimmer.fromColors( 37 | baseColor: Theme.of(context).cardColor.withOpacity(0.4), 38 | highlightColor: Theme.of(context).cardColor, 39 | child: Container( 40 | width: 65, 41 | height: 65, 42 | decoration: BoxDecoration( 43 | borderRadius: BorderRadius.circular(50), 44 | color: Theme.of(context).cardColor.withOpacity(0.4) 45 | ), 46 | ), 47 | ), 48 | Shimmer.fromColors( 49 | baseColor: Theme.of(context).cardColor.withOpacity(0.4), 50 | highlightColor: Theme.of(context).cardColor, 51 | child: Container( 52 | width: 65, 53 | height: 65, 54 | decoration: BoxDecoration( 55 | borderRadius: BorderRadius.circular(50), 56 | color: Theme.of(context).cardColor.withOpacity(0.4) 57 | ), 58 | ), 59 | ), 60 | Shimmer.fromColors( 61 | baseColor: Theme.of(context).cardColor.withOpacity(0.4), 62 | highlightColor: Theme.of(context).cardColor, 63 | child: Container( 64 | width: 65, 65 | height: 65, 66 | decoration: BoxDecoration( 67 | borderRadius: BorderRadius.circular(50), 68 | color: Theme.of(context).cardColor.withOpacity(0.4) 69 | ), 70 | ), 71 | ), 72 | SizedBox(height: 8) 73 | ], 74 | ); 75 | } 76 | } -------------------------------------------------------------------------------- /lib/screens/musicScreen/tabs/genre.dart: -------------------------------------------------------------------------------- 1 | import 'package:animations/animations.dart'; 2 | import 'package:audio_service/audio_service.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:songtube/internal/models/mediaItemSorts.dart'; 5 | import 'package:songtube/screens/musicScreen/components/music_type_expandable.dart'; 6 | import 'package:songtube/screens/musicScreen/components/songsList.dart'; 7 | 8 | class MusicScreenGenreTab extends StatefulWidget { 9 | final List songs; 10 | final String searchQuery; 11 | MusicScreenGenreTab({ 12 | this.songs, 13 | this.searchQuery 14 | }); 15 | @override 16 | _MusicScreenGenreTabState createState() => _MusicScreenGenreTabState(); 17 | } 18 | 19 | class _MusicScreenGenreTabState extends State { 20 | // Genres 21 | List _genres = []; 22 | 23 | // Current Genre 24 | MediaItemGenre currentGenre; 25 | 26 | // Genres GridView Key 27 | final genresGridKey = const PageStorageKey('songsGenreList'); 28 | 29 | @override 30 | void initState() { 31 | widget.songs.forEach((song) => songCreateOrAssignToGenre(song)); 32 | super.initState(); 33 | } 34 | 35 | void songCreateOrAssignToGenre(MediaItem song) { 36 | // Check if Genre exist and create it 37 | if (_genres.indexWhere((genre) => genre.genreName == (song?.genre ?? "unknown")) == -1) { 38 | // Create Genre and add the song 39 | _genres.add( 40 | MediaItemGenre( 41 | genreName: song?.genre ?? "unknown", 42 | mediaItems: [] 43 | ) 44 | ); 45 | } 46 | // Add song to Genre 47 | int indexToGenre = _genres.indexWhere((genre) { 48 | return genre.genreName == (song?.genre ?? "unknown"); 49 | }); 50 | _genres[indexToGenre].mediaItems.add(song); 51 | } 52 | 53 | Widget build(BuildContext context) { 54 | List genres = []; 55 | for (int i = 0; i < _genres.length; i++) { 56 | if (widget.searchQuery == "" || getSearchQueryMatch(_genres[i])) 57 | genres.add(_genres[i]); 58 | } 59 | return ListView.builder( 60 | key: genresGridKey, 61 | itemCount: genres.length, 62 | itemBuilder: (context, index) { 63 | MediaItemGenre genre = genres[index]; 64 | return MusicScreenTypeExpandable( 65 | title: genre.genreName, 66 | songs: genre.mediaItems, 67 | ); 68 | } 69 | ); 70 | } 71 | 72 | bool getSearchQueryMatch(MediaItemGenre genre) { 73 | if (widget.searchQuery != "") { 74 | if (genre.genreName.toLowerCase().contains(widget.searchQuery.toLowerCase())) { 75 | return true; 76 | } else if (genre.genreName.toLowerCase().contains(widget.searchQuery.toLowerCase())) { 77 | return true; 78 | } else { 79 | return false; 80 | } 81 | } else { 82 | return true; 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /lib/pages/components/localVideos/folderGridView.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | // Internal 5 | import 'package:songtube/internal/models/folder.dart'; 6 | 7 | // Packages 8 | import 'package:eva_icons_flutter/eva_icons_flutter.dart'; 9 | 10 | class FolderGridView extends StatelessWidget { 11 | final List list; 12 | final Function(FolderItem) onFolderTap; 13 | FolderGridView({ 14 | @required this.list, 15 | @required this.onFolderTap 16 | }); 17 | @override 18 | Widget build(BuildContext context) { 19 | return GridView.builder( 20 | 21 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2), 22 | itemCount: list.length, 23 | itemBuilder: (context, index) { 24 | FolderItem folder = list[index]; 25 | return Padding( 26 | padding: EdgeInsets.only( 27 | top: index == 0 || index == 1 ? 16 : 8, 28 | bottom: 8, 29 | left: 8, 30 | right: 8 31 | ), 32 | child: InkWell( 33 | borderRadius: BorderRadius.circular(20), 34 | splashColor: Theme.of(context).accentColor.withOpacity(0.1), 35 | onTap: () => onFolderTap(folder), 36 | child: Ink( 37 | decoration: BoxDecoration( 38 | borderRadius: BorderRadius.circular(20), 39 | color: Theme.of(context).scaffoldBackgroundColor, 40 | border: Border.all(color: Colors.grey[600].withOpacity(0.1)) 41 | ), 42 | child: Column( 43 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 44 | crossAxisAlignment: CrossAxisAlignment.center, 45 | children: [ 46 | Container( 47 | margin: EdgeInsets.only(top: 10), 48 | child: Icon( 49 | EvaIcons.folderOutline, 50 | size: 70, 51 | color: Theme.of(context).iconTheme.color.withOpacity(0.5) 52 | ) 53 | ), 54 | Container( 55 | margin: EdgeInsets.only(left: 16, right: 16), 56 | child: Text( 57 | folder.name == "0" ? "Internal Storage" : folder.name, 58 | maxLines: 1, 59 | style: TextStyle( 60 | color: Theme.of(context).textTheme.bodyText1.color.withOpacity(0.8), 61 | fontSize: 14, 62 | fontFamily: 'Product Sans', 63 | fontWeight: FontWeight.w600 64 | ), 65 | textAlign: TextAlign.center, 66 | ), 67 | ) 68 | ], 69 | ), 70 | ), 71 | ), 72 | ); 73 | }, 74 | ); 75 | } 76 | } -------------------------------------------------------------------------------- /lib/internal/models/tagsControllers.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:newpipeextractor_dart/models/infoItems/video.dart'; 3 | import 'package:newpipeextractor_dart/models/playlist.dart'; 4 | import 'package:newpipeextractor_dart/models/video.dart'; 5 | 6 | class TagsControllers { 7 | 8 | TagsControllers() { 9 | // Controllers 10 | urlController = new TextEditingController(); 11 | titleController = new TextEditingController(); 12 | albumController = new TextEditingController(); 13 | artistController = new TextEditingController(); 14 | genreController = new TextEditingController(); 15 | dateController = new TextEditingController(); 16 | discController = new TextEditingController(); 17 | trackController = new TextEditingController(); 18 | } 19 | 20 | TextEditingController urlController; 21 | TextEditingController titleController; 22 | TextEditingController albumController; 23 | TextEditingController artistController; 24 | TextEditingController genreController; 25 | TextEditingController dateController; 26 | TextEditingController discController; 27 | TextEditingController trackController; 28 | String artworkController; 29 | 30 | void updateTextControllers(YoutubeVideo stream) { 31 | titleController.text = stream.videoInfo.name; 32 | albumController.text = "YouTube"; 33 | artistController.text = stream.videoInfo.uploaderName 34 | .replaceAll("- Topic", "") 35 | .trim(); 36 | genreController.text = "Any"; 37 | dateController.text = stream.videoInfo.uploadDate; 38 | discController.text = "1"; 39 | trackController.text = "1"; 40 | artworkController = stream.videoInfo.thumbnailUrl; 41 | } 42 | 43 | void updateTextControllersFromPlaylist(YoutubePlaylist playlist) { 44 | titleController.text = playlist.name; 45 | albumController.text = "YouTube"; 46 | artistController.text = playlist.uploaderName; 47 | genreController.text = "Any"; 48 | dateController.text = "${DateTime.now().year}/" 49 | + "${DateTime.now().month}" 50 | + "${DateTime.now().day}"; 51 | discController.text = "1"; 52 | trackController.text = "1"; 53 | artworkController = playlist.thumbnailUrl; 54 | } 55 | 56 | void updateTextControllersFromStream(StreamInfoItem stream) { 57 | titleController.text = stream.name; 58 | albumController.text = "YouTube"; 59 | artistController.text = stream.uploaderName; 60 | genreController.text = "Any"; 61 | dateController.text = "${DateTime.now().year}/" 62 | + "${DateTime.now().month}" 63 | + "${DateTime.now().day}"; 64 | discController.text = "1"; 65 | trackController.text = "1"; 66 | artworkController = stream.thumbnails.hqdefault; 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /lib/screens/musicScreen/tabs/artist.dart: -------------------------------------------------------------------------------- 1 | import 'package:animations/animations.dart'; 2 | import 'package:audio_service/audio_service.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:songtube/internal/models/mediaItemSorts.dart'; 5 | import 'package:songtube/screens/musicScreen/components/music_type_expandable.dart'; 6 | import 'package:songtube/screens/musicScreen/components/songsList.dart'; 7 | 8 | class MusicScreenArtistTab extends StatefulWidget { 9 | final List songs; 10 | final String searchQuery; 11 | MusicScreenArtistTab({ 12 | this.songs, 13 | this.searchQuery 14 | }); 15 | @override 16 | _MusicScreenArtistTabState createState() => _MusicScreenArtistTabState(); 17 | } 18 | 19 | class _MusicScreenArtistTabState extends State { 20 | 21 | // Artists 22 | List _artists = []; 23 | 24 | // Current Artist 25 | MediaItemArtist currentArtist; 26 | 27 | // Artists GridView Key 28 | final artistsGridKey = const PageStorageKey('songsArtistList'); 29 | 30 | @override 31 | void initState() { 32 | widget.songs.forEach((song) => songCreateOrAssignToArtist(song)); 33 | super.initState(); 34 | } 35 | 36 | void songCreateOrAssignToArtist(MediaItem song) { 37 | // Check if Artist exist and create it 38 | if (_artists.indexWhere((artist) => artist.artistName == (song?.artist ?? "unknown")) == -1) { 39 | // Create Artist and add the song 40 | _artists.add( 41 | MediaItemArtist( 42 | artistName: song?.artist ?? "unknown", 43 | mediaItems: [] 44 | ) 45 | ); 46 | } 47 | // Add song to Artist 48 | int indexToArtist = _artists.indexWhere((artist) => artist.artistName == (song?.artist ?? "unknown")); 49 | _artists[indexToArtist].mediaItems.add(song); 50 | } 51 | 52 | Widget build(BuildContext context) { 53 | List artists = []; 54 | for (int i = 0; i < _artists.length; i++) { 55 | if (widget.searchQuery == "" || getSearchQueryMatch(_artists[i])) 56 | artists.add(_artists[i]); 57 | } 58 | return ListView.builder( 59 | key: artistsGridKey, 60 | itemCount: artists.length, 61 | itemBuilder: (context, index) { 62 | MediaItemArtist artist = artists[index]; 63 | return MusicScreenTypeExpandable( 64 | title: artist.artistName, 65 | songs: artist.mediaItems, 66 | ); 67 | } 68 | ); 69 | } 70 | 71 | bool getSearchQueryMatch(MediaItemArtist artist) { 72 | if (widget.searchQuery != "") { 73 | if (artist.artistName.toLowerCase().contains(widget.searchQuery.toLowerCase())) { 74 | return true; 75 | } else if (artist.artistName.toLowerCase().contains(widget.searchQuery.toLowerCase())) { 76 | return true; 77 | } else { 78 | return false; 79 | } 80 | } else { 81 | return true; 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /lib/players/components/youtubePlayer/ui/details.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intl/intl.dart'; 3 | import 'package:newpipeextractor_dart/models/infoItems/video.dart'; 4 | 5 | class VideoDetails extends StatelessWidget { 6 | final dynamic infoItem; 7 | final Function onMoreDetails; 8 | VideoDetails({ 9 | @required this.infoItem, 10 | this.onMoreDetails 11 | }); 12 | @override 13 | Widget build(BuildContext context) { 14 | String title = infoItem?.name ?? ""; 15 | String views = "${NumberFormat.compact().format(infoItem is StreamInfoItem ? infoItem?.viewCount : 0)} views" ?? ""; 16 | String date = (infoItem is StreamInfoItem ? infoItem?.uploadDate : "") ?? ""; 17 | return Column( 18 | children: [ 19 | Container( 20 | margin: EdgeInsets.only( 21 | left: 14, right: 12, 22 | ), 23 | child: Row( 24 | mainAxisAlignment: MainAxisAlignment.start, 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | Expanded( 28 | child: Column( 29 | crossAxisAlignment: CrossAxisAlignment.start, 30 | children: [ 31 | // Video Title 32 | Padding( 33 | padding: const EdgeInsets.only(right: 16, bottom: 4), 34 | child: Text( 35 | title, 36 | style: TextStyle( 37 | fontSize: 20, 38 | fontWeight: FontWeight.w600, 39 | fontFamily: 'Product Sans', 40 | color: Theme.of(context).textTheme.bodyText1.color, 41 | ), 42 | textAlign: TextAlign.left, 43 | maxLines: 2, 44 | overflow: TextOverflow.ellipsis, 45 | ), 46 | ), 47 | // Video Author 48 | Padding( 49 | padding: const EdgeInsets.only(right: 16, bottom: 8), 50 | child: Text( 51 | (views.contains('-1') ? "" : (views + " • ")) + date, 52 | style: TextStyle( 53 | color: Theme.of(context).textTheme.bodyText1.color 54 | .withOpacity(0.8), 55 | fontFamily: "Product Sans", 56 | fontSize: 12, 57 | letterSpacing: 0.2 58 | ), 59 | ), 60 | ), 61 | ], 62 | ), 63 | ), 64 | IconButton( 65 | icon: Icon(Icons.expand_more_rounded, 66 | color: Theme.of(context).iconTheme.color), 67 | onPressed: onMoreDetails, 68 | ) 69 | ], 70 | ), 71 | ), 72 | ], 73 | ); 74 | } 75 | 76 | 77 | 78 | } -------------------------------------------------------------------------------- /lib/pages/localVideos.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:songtube/internal/models/folder.dart'; 4 | import 'package:songtube/internal/models/videoFile.dart'; 5 | import 'package:songtube/pages/components/localVideos/folderGridView.dart'; 6 | import 'package:songtube/pages/components/localVideos/videosOnFolderListView.dart'; 7 | import 'package:songtube/players/videoPlayer.dart'; 8 | import 'package:songtube/provider/mediaProvider.dart'; 9 | 10 | class LocalVideosPage extends StatefulWidget { 11 | @override 12 | _LocalVideosPageState createState() => _LocalVideosPageState(); 13 | } 14 | 15 | class _LocalVideosPageState extends State { 16 | 17 | // Current Viewing Folder 18 | FolderItem folderOnView; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | MediaProvider mediaProvider = Provider.of(context); 23 | return Scaffold( 24 | appBar: AppBar( 25 | backgroundColor: Theme.of(context).cardColor, 26 | title: Text( 27 | "Local Videos", 28 | style: TextStyle( 29 | fontFamily: 'Product Sans', 30 | fontWeight: FontWeight.w600, 31 | fontSize: 24, 32 | color: Theme.of(context).textTheme.bodyText1.color 33 | ), 34 | ), 35 | leading: IconButton( 36 | icon: Icon(Icons.arrow_back_ios_new_rounded, color: Theme.of(context).iconTheme.color), 37 | onPressed: () { 38 | Navigator.pop(context); 39 | }, 40 | ), 41 | elevation: 0, 42 | ), 43 | body: Column( 44 | children: [ 45 | Divider( 46 | height: 1, 47 | thickness: 1, 48 | color: Colors.grey[600].withOpacity(0.1), 49 | indent: 12, 50 | endIndent: 12 51 | ), 52 | Expanded( 53 | child: AnimatedSwitcher( 54 | duration: Duration(milliseconds: 300), 55 | child: folderOnView == null 56 | ? FolderGridView( 57 | list: mediaProvider.listFolders, 58 | onFolderTap: (FolderItem selectedFolder) { 59 | setState(() => folderOnView = selectedFolder); 60 | } 61 | ) 62 | : WillPopScope( 63 | onWillPop: () { 64 | setState(() => folderOnView = null); 65 | return Future.value(false); 66 | }, 67 | child: VideosOnFolderListView( 68 | list: folderOnView.videos, 69 | onVideoTap: (VideoFile video) { 70 | Navigator.push( 71 | context, 72 | MaterialPageRoute(builder: (context) => 73 | AppVideoPlayer(video)) 74 | ); 75 | } 76 | ), 77 | ), 78 | ), 79 | ), 80 | ], 81 | ), 82 | ); 83 | } 84 | } -------------------------------------------------------------------------------- /lib/ui/animations/showUp.dart: -------------------------------------------------------------------------------- 1 | // Dart 2 | import 'dart:async'; 3 | 4 | // Flutter 5 | import 'package:flutter/material.dart'; 6 | 7 | enum SlideFromSlide {TOP, BOTTOM, LEFT, RIGHT} 8 | 9 | class ShowUpTransition extends StatefulWidget { 10 | /// [child] to be Animated 11 | final Widget child; 12 | /// Animation Duration, default is 200 Milliseconds 13 | final Duration duration; 14 | /// Delay before starting Animation, default is Zero 15 | final Duration delay; 16 | /// Bring forward/reverse the Animation 17 | final bool forward; 18 | /// From which direction start the [Slide] animation 19 | final SlideFromSlide slideSide; 20 | 21 | ShowUpTransition({ 22 | @required this.child, 23 | this.duration, 24 | this.delay, 25 | this.slideSide = SlideFromSlide.LEFT, 26 | @required this.forward 27 | }); 28 | 29 | @override 30 | _ShowUpTransitionState createState() => _ShowUpTransitionState(); 31 | } 32 | 33 | class _ShowUpTransitionState extends State 34 | with SingleTickerProviderStateMixin { 35 | 36 | AnimationController _animController; 37 | Animation _animOffset; 38 | 39 | List slideSides = [ 40 | Offset(-0.20,0.0), // LEFT 41 | Offset(0.20,0.0), // RIGHT 42 | Offset(0.0,0.20), // BOTTOM 43 | Offset(0.0,-0.20), // TOP 44 | ]; 45 | Offset selectedSlide; 46 | 47 | 48 | 49 | @override 50 | void initState() { 51 | super.initState(); 52 | _animController = 53 | AnimationController(vsync: this, duration: widget.duration == null 54 | ? Duration(milliseconds: 400) 55 | : widget.duration 56 | ); 57 | switch (widget.slideSide) { 58 | case SlideFromSlide.LEFT: 59 | selectedSlide = slideSides[0]; break; 60 | case SlideFromSlide.RIGHT: 61 | selectedSlide = slideSides[1]; break; 62 | case SlideFromSlide.BOTTOM: 63 | selectedSlide = slideSides[2]; break; 64 | case SlideFromSlide.TOP: 65 | selectedSlide = slideSides[3]; break; 66 | } 67 | _animOffset = 68 | Tween( 69 | begin: selectedSlide, 70 | end: Offset.zero 71 | ).animate(CurvedAnimation( 72 | curve: Curves.fastLinearToSlowEaseIn, 73 | parent: _animController 74 | )); 75 | } 76 | 77 | @override 78 | void dispose() { 79 | _animController.dispose(); 80 | super.dispose(); 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | Timer(widget.delay == null ? Duration.zero : widget.delay, () { 86 | if (widget.forward) { 87 | if (mounted) 88 | _animController.forward(); 89 | } else { 90 | if (mounted) 91 | _animController.reverse(); 92 | } 93 | }); 94 | return widget.forward 95 | ? FadeTransition( 96 | child: SlideTransition( 97 | position: _animOffset, 98 | child: widget.child, 99 | ), 100 | opacity: _animController, 101 | ) 102 | : Container(); 103 | } 104 | } -------------------------------------------------------------------------------- /lib/internal/avatarHandler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:newpipeextractor_dart/extractors/channels.dart'; 5 | import 'package:newpipeextractor_dart/utils/url.dart'; 6 | import 'package:path_provider/path_provider.dart'; 7 | import 'package:http/http.dart' as http; 8 | 9 | class AvatarHandler { 10 | 11 | /// Returns a file path string associated to this channel name which 12 | /// represents the avatar image as [File] type, if the avatar image does 13 | /// not exist it will be downloaded and written to its file and if it 14 | /// does exist this function will just return that file path string. 15 | /// 16 | /// If you want to update the cached image, save and return a new image, 17 | /// set [updateAvatar] to true. 18 | static Future getAvatarUrl(String channelName, String channelUrl, {bool updateAvatar = false}) async { 19 | 20 | // Create our dirs and define our avatar file path 21 | Directory avatarDir = Directory((await getApplicationDocumentsDirectory()).path + "/avatarDir/"); 22 | if (!(await avatarDir.exists())) avatarDir.create(recursive: true); 23 | File avatarImage = File(avatarDir.path + "/$channelName"); 24 | 25 | // Return avatar image file path if it exist 26 | if (await avatarImage.exists() && !updateAvatar) return avatarImage.path; 27 | 28 | // Extract the avatar image from the channel url provided using our Isolate 29 | String id = (await YoutubeId.getIdFromChannelUrl(channelUrl)).split("/").last; 30 | ReceivePort receivePort = ReceivePort(); 31 | await Isolate.spawn(_getChannelLogoUrlIsolate, receivePort.sendPort); 32 | SendPort childSendPort = await receivePort.first; 33 | ReceivePort responsePort = ReceivePort(); 34 | childSendPort.send([id, responsePort.sendPort]); 35 | String imageUrl = await responsePort.first; 36 | 37 | // Create our avatar image file 38 | http.Client client = http.Client(); 39 | try { 40 | var response = await client.get(Uri.parse(imageUrl)); 41 | if (response.statusCode != 200) return null; 42 | await avatarImage.writeAsBytes(response.bodyBytes); 43 | client.close(); 44 | } catch (_) { client.close(); return null; } 45 | 46 | // Return newly created avatar Image, any other request to the same 47 | // Channel avatar image will just return this cached image 48 | return avatarImage.path; 49 | } 50 | 51 | // Since parsing the channel avatar image url is CPU expensive, we will 52 | // use this isolate to take out the work off the main thread 53 | static void _getChannelLogoUrlIsolate(SendPort mainSendPort) async { 54 | ReceivePort childReceivePort = ReceivePort(); 55 | mainSendPort.send(childReceivePort.sendPort); 56 | await for (var message in childReceivePort) { 57 | String videoId = message[0]; 58 | SendPort replyPort = message[1]; 59 | String avatarUrl; 60 | try { 61 | avatarUrl = await ChannelExtractor.getAvatarUrl(videoId); 62 | } catch (_) { 63 | replyPort.send(""); 64 | break; 65 | } 66 | replyPort.send(avatarUrl); 67 | break; 68 | } 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /lib/ui/components/searchBar.dart: -------------------------------------------------------------------------------- 1 | import 'package:eva_icons_flutter/eva_icons_flutter.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class CommonSearchBar extends StatelessWidget { 5 | final TextEditingController textController; 6 | final FocusNode focusNode; 7 | final Function(String) onChanged; 8 | final Function(String) onSubmit; 9 | final Function onClear; 10 | final String hintText; 11 | CommonSearchBar({ 12 | @required this.textController, 13 | @required this.focusNode, 14 | @required this.onChanged, 15 | @required this.onClear, 16 | this.onSubmit, 17 | this.hintText = "" 18 | }); 19 | @override 20 | Widget build(BuildContext context) { 21 | return Container( 22 | width: double.infinity, 23 | height: kToolbarHeight*0.8, 24 | decoration: BoxDecoration( 25 | color: Theme.of(context).cardColor 26 | ), 27 | child: Container( 28 | decoration: BoxDecoration( 29 | color: Theme.of(context).scaffoldBackgroundColor, 30 | borderRadius: BorderRadius.circular(20) 31 | ), 32 | child: Theme( 33 | data: ThemeData(primaryColor: Theme.of(context).accentColor), 34 | child: Stack( 35 | children: [ 36 | TextField( 37 | keyboardType: TextInputType.url, 38 | style: TextStyle( 39 | color: Theme.of(context).textTheme.bodyText1.color, 40 | fontSize: 14 41 | ), 42 | focusNode: focusNode, 43 | controller: textController, 44 | decoration: InputDecoration( 45 | contentPadding: EdgeInsets.all(14.0), 46 | prefixIcon: Icon(EvaIcons.searchOutline, size: 22, color: Colors.red), 47 | hintText: hintText, 48 | hintStyle: TextStyle( 49 | color: Theme.of(context).textTheme.bodyText1.color.withOpacity(0.4), 50 | fontSize: 14 51 | ), 52 | border: UnderlineInputBorder( 53 | borderRadius: BorderRadius.circular(10), 54 | borderSide: BorderSide( 55 | width: 0, 56 | style: BorderStyle.none, 57 | ), 58 | ), 59 | ), 60 | onChanged: (String searchQuery) => onChanged(searchQuery), 61 | onSubmitted: (searchQuery) { 62 | onSubmit(searchQuery); 63 | FocusScope.of(context).unfocus(); 64 | } 65 | ), 66 | Align( 67 | alignment: Alignment.centerRight, 68 | child: textController.text != "" 69 | ? IconButton( 70 | icon: Icon( 71 | Icons.clear, 72 | size: 20, 73 | color: Theme.of(context).iconTheme.color 74 | ), 75 | onPressed: onClear 76 | ) 77 | : Container(), 78 | ) 79 | ], 80 | ), 81 | ), 82 | ) 83 | ); 84 | } 85 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/ui/components/autoHideScaffold.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AutoHideScaffold extends StatefulWidget { 4 | final Widget appBar; 5 | final Widget body; 6 | final bool resizeToAvoidBottomInset; 7 | final Color backgroundColor; 8 | AutoHideScaffold({ 9 | this.appBar, 10 | @required this.body, 11 | this.resizeToAvoidBottomInset, 12 | this.backgroundColor, 13 | Key key 14 | }) : super(key: key); 15 | 16 | @override 17 | _AutoHideScaffoldState createState() => _AutoHideScaffoldState(); 18 | } 19 | 20 | class _AutoHideScaffoldState extends State with TickerProviderStateMixin { 21 | 22 | AnimationController _hideAnimationController; 23 | bool navigationBarScrolledDown = false; 24 | 25 | // Pixels Scrolled 26 | double pixelsScrolled = 0; 27 | 28 | @override 29 | void initState() { 30 | _hideAnimationController = AnimationController( 31 | duration: Duration(milliseconds: 250), vsync: this 32 | ); 33 | _hideAnimationController.animateTo(1, duration: Duration.zero); 34 | super.initState(); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return AnimatedBuilder( 40 | animation: _hideAnimationController, 41 | builder: (context, child) { 42 | return Scaffold( 43 | key: widget.key, 44 | backgroundColor: widget.backgroundColor, 45 | resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset, 46 | appBar: widget.appBar != null ? PreferredSize( 47 | preferredSize: Size( 48 | double.infinity, 49 | kToolbarHeight * _hideAnimationController.value 50 | ), 51 | child: Container( 52 | color: Theme.of(context).cardColor, 53 | child: Opacity( 54 | opacity: (_hideAnimationController.value - (1 - _hideAnimationController.value)) > 0 55 | ? (_hideAnimationController.value - (1 - _hideAnimationController.value)) : 0, 56 | child: widget.appBar 57 | ), 58 | ), 59 | ): null, 60 | body: child, 61 | ); 62 | }, 63 | child: Listener( 64 | onPointerUp: (_) { 65 | pixelsScrolled = 0; 66 | if (_hideAnimationController.value > 0.5) { 67 | _hideAnimationController.animateTo(1); 68 | navigationBarScrolledDown = false; 69 | } else { 70 | _hideAnimationController.animateTo(0); 71 | navigationBarScrolledDown = true; 72 | } 73 | }, 74 | child: NotificationListener( 75 | onNotification: (ScrollUpdateNotification details) { 76 | pixelsScrolled = (pixelsScrolled + details.scrollDelta.abs()).clamp(0, 100)/100; 77 | if (details.scrollDelta > 0.0 && details.metrics.axis == Axis.vertical) { 78 | _hideAnimationController.value -= pixelsScrolled; 79 | } else { 80 | _hideAnimationController.value += pixelsScrolled; 81 | } 82 | return false; 83 | }, 84 | child: widget.body 85 | ), 86 | ), 87 | ); 88 | } 89 | } -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | 9 | 10 | 13 | 18 | 23 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /lib/players/components/musicPlayer/expandedPanel.dart: -------------------------------------------------------------------------------- 1 | // Dart 2 | import 'dart:io'; 3 | import 'dart:ui'; 4 | 5 | // Flutter 6 | import 'package:audio_service/audio_service.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | // Internal 11 | import 'package:songtube/players/service/playerService.dart'; 12 | import 'package:songtube/players/components/musicPlayer/ui/playerBackground.dart'; 13 | import 'package:songtube/players/components/musicPlayer/ui/playerBody.dart'; 14 | import 'package:songtube/players/service/screenStateStream.dart'; 15 | import 'package:songtube/provider/configurationProvider.dart'; 16 | import 'package:songtube/provider/mediaProvider.dart'; 17 | 18 | // Packages 19 | import 'package:songtube/provider/preferencesProvider.dart'; 20 | import 'package:songtube/ui/components/fancyScaffold.dart'; 21 | 22 | class ExpandedPlayer extends StatelessWidget { 23 | static FloatingWidgetConfig config () { 24 | return FloatingWidgetConfig( 25 | 26 | ); 27 | } 28 | @override 29 | Widget build(BuildContext context) { 30 | return StreamBuilder( 31 | stream: screenStateStream, 32 | builder: (context, snapshot) { 33 | final screenState = snapshot.data; 34 | final mediaItem = screenState?.mediaItem; 35 | final state = screenState?.playbackState; 36 | final playing = state?.playing ?? false; 37 | PreferencesProvider prefs = Provider.of(context); 38 | ConfigurationProvider config = Provider.of(context); 39 | MediaProvider mediaProvider = Provider.of(context); 40 | File image = mediaProvider.artwork; 41 | Color dominantColor = prefs.enablePlayerBlurBackground 42 | ? mediaProvider.dominantColor == null ? Colors.white : mediaProvider.dominantColor 43 | : Theme.of(context).accentColor; 44 | Color textColor = prefs.enablePlayerBlurBackground 45 | ? dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white 46 | : Theme.of(context).textTheme.bodyText1.color; 47 | Color vibrantColor = prefs.enablePlayerBlurBackground 48 | ? mediaProvider.vibrantColor == null ? Colors.white : mediaProvider.vibrantColor 49 | : Theme.of(context).accentColor; 50 | return PlayerBackground( 51 | backgroundImage: File(AudioService.currentMediaItem?.extras["artwork"] ?? ''), 52 | enableBlur: prefs.enablePlayerBlurBackground, 53 | blurIntensity: 50, 54 | backdropColor: prefs.enablePlayerBlurBackground 55 | ? dominantColor 56 | : Theme.of(context).scaffoldBackgroundColor, 57 | backdropOpacity: 0.4, 58 | child: PlayerBody( 59 | controller: mediaProvider.fwController, 60 | playingFrom: mediaItem?.album ?? '', 61 | textColor: textColor, 62 | artworkFile: image, 63 | vibrantColor: vibrantColor, 64 | playing: playing, 65 | mediaItem: mediaItem, 66 | dominantColor: dominantColor, 67 | state: state, 68 | expandArtwork: config.useExpandedArtwork, 69 | ) 70 | ); 71 | } 72 | ); 73 | } 74 | } -------------------------------------------------------------------------------- /lib/ui/animations/blurPageRoute.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:songtube/ui/animations/FadeIn.dart'; 6 | 7 | class BlurPageRoute extends PageRoute with MaterialRouteTransitionMixin { 8 | 9 | Duration duration; 10 | bool keepState; 11 | double blurStrength; 12 | Curve animationCurve; 13 | Curve exitAnimationCurve; 14 | Offset slideOffset; 15 | bool useCardExit; 16 | Color backdropColor; 17 | 18 | BlurPageRoute({ 19 | this.duration = const Duration(milliseconds: 500), 20 | this.keepState = false, 21 | this.blurStrength = 0, 22 | @required this.builder, 23 | RouteSettings settings, 24 | this.maintainState = true, 25 | bool fullscreenDialog = false, 26 | this.animationCurve = Curves.fastLinearToSlowEaseIn, 27 | this.exitAnimationCurve = Curves.linearToEaseOut, 28 | this.opaque = false, 29 | this.slideOffset = const Offset(0.0, 10.0), 30 | this.useCardExit = true, 31 | this.backdropColor = Colors.transparent 32 | }) : assert(builder != null), 33 | assert(maintainState != null), 34 | assert(fullscreenDialog != null), 35 | super(settings: settings, fullscreenDialog: fullscreenDialog); 36 | 37 | /// Builds the primary contents of the route. 38 | final WidgetBuilder builder; 39 | 40 | @override 41 | Widget buildContent(BuildContext context) => builder(context); 42 | 43 | @override 44 | Widget buildTransitions(BuildContext context, Animation animation, 45 | Animation secondaryAnimation, Widget child) { 46 | // Create transition from bottom to top, like bottom sheet 47 | if (animation.status == AnimationStatus.reverse) { 48 | return BackdropFilter( 49 | filter: ImageFilter.blur( 50 | sigmaX: blurStrength*animation.value, 51 | sigmaY: blurStrength*animation.value 52 | ), 53 | child: SlideTransition( 54 | position: CurvedAnimation( 55 | parent: animation, 56 | curve: exitAnimationCurve, 57 | ).drive( 58 | Tween( 59 | begin: slideOffset, 60 | end: Offset(0.0, 0.0), 61 | ), 62 | ), 63 | child: child, 64 | ), 65 | ); 66 | } else { 67 | return SlideTransition( 68 | position: CurvedAnimation( 69 | parent: animation, 70 | curve: animationCurve, 71 | ).drive( 72 | Tween( 73 | begin: slideOffset, 74 | end: Offset(0.0, 0.0), 75 | ), 76 | ), 77 | child: BackdropFilter( 78 | filter: ImageFilter.blur( 79 | sigmaX: blurStrength*animation.value, 80 | sigmaY: blurStrength*animation.value 81 | ), 82 | child: FadeInTransition( 83 | duration: const Duration(milliseconds: 300), 84 | child: child 85 | ) 86 | ), 87 | ); 88 | } 89 | } 90 | 91 | @override 92 | final bool maintainState; 93 | 94 | @override 95 | Duration get transitionDuration => duration; 96 | 97 | @override 98 | Color get barrierColor => backdropColor; 99 | 100 | @override 101 | bool opaque; 102 | 103 | } -------------------------------------------------------------------------------- /lib/pages/components/video/shimmer/shimmerVideoTile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shimmer/shimmer.dart'; 3 | 4 | class ShimmerVideoTile extends StatelessWidget { 5 | const ShimmerVideoTile(); 6 | @override 7 | Widget build(BuildContext context) { 8 | return Padding( 9 | padding: EdgeInsets.only(bottom: 16), 10 | child: Column( 11 | children: [ 12 | Container( 13 | margin: EdgeInsets.only(left: 12, right: 12), 14 | child: ClipRRect( 15 | borderRadius: BorderRadius.circular(10), 16 | child: Shimmer.fromColors( 17 | baseColor: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.6), 18 | highlightColor: Theme.of(context).cardColor, 19 | child: AspectRatio( 20 | aspectRatio: 16/9, 21 | child: Container( 22 | decoration: BoxDecoration( 23 | color: Theme.of(context).scaffoldBackgroundColor 24 | ), 25 | ), 26 | ), 27 | ), 28 | ), 29 | ), 30 | Container( 31 | margin: EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 4), 32 | child: Row( 33 | children: [ 34 | Shimmer.fromColors( 35 | baseColor: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.6), 36 | highlightColor: Theme.of(context).cardColor, 37 | child: Container( 38 | height: 60, 39 | width: 60, 40 | margin: EdgeInsets.only(right: 8), 41 | decoration: BoxDecoration( 42 | borderRadius: BorderRadius.circular(100), 43 | color: Theme.of(context).scaffoldBackgroundColor 44 | ), 45 | ), 46 | ), 47 | Expanded( 48 | child: Column( 49 | children: [ 50 | Shimmer.fromColors( 51 | baseColor: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.6), 52 | highlightColor: Theme.of(context).cardColor, 53 | child: Container( 54 | height: 20, 55 | decoration: BoxDecoration( 56 | borderRadius: BorderRadius.circular(5), 57 | color: Theme.of(context).scaffoldBackgroundColor 58 | ), 59 | ), 60 | ), 61 | SizedBox(height: 8), 62 | Shimmer.fromColors( 63 | baseColor: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.6), 64 | highlightColor: Theme.of(context).cardColor, 65 | child: Container( 66 | height: 20, 67 | decoration: BoxDecoration( 68 | borderRadius: BorderRadius.circular(5), 69 | color: Theme.of(context).scaffoldBackgroundColor 70 | ), 71 | ), 72 | ), 73 | ], 74 | ), 75 | ) 76 | ], 77 | ), 78 | ) 79 | ], 80 | ), 81 | ); 82 | } 83 | } -------------------------------------------------------------------------------- /lib/ui/internal/snackbar.dart: -------------------------------------------------------------------------------- 1 | // Flutter 2 | import 'package:flutter/material.dart'; 3 | 4 | // Internal 5 | import 'package:songtube/players/service/playerService.dart'; 6 | import 'package:songtube/players/service/screenStateStream.dart'; 7 | 8 | // Packages 9 | import 'package:audio_service/audio_service.dart'; 10 | 11 | class AppSnack { 12 | 13 | // Show SnackBar with Icon, Title and Message 14 | static void showSnackBar({ 15 | @required IconData icon, 16 | @required String title, 17 | String message, 18 | Duration duration = const Duration(seconds: 2), 19 | @required context, 20 | scaffoldKey 21 | }) { 22 | if (scaffoldKey == null) 23 | ScaffoldMessenger.of(context).removeCurrentSnackBar(); 24 | else 25 | scaffoldKey.removeCurrentSnackBar(); 26 | final snack = SnackBar( 27 | content: Column( 28 | mainAxisAlignment: MainAxisAlignment.start, 29 | crossAxisAlignment: CrossAxisAlignment.start, 30 | mainAxisSize: MainAxisSize.min, 31 | children: [ 32 | Row( 33 | mainAxisSize: MainAxisSize.min, 34 | children: [ 35 | Icon( 36 | icon, 37 | color: Theme.of(context).accentColor, 38 | ), 39 | SizedBox(width: 8), 40 | Expanded( 41 | child: Column( 42 | mainAxisSize: MainAxisSize.min, 43 | crossAxisAlignment: CrossAxisAlignment.start, 44 | children: [ 45 | SizedBox(height: 4), 46 | Text( 47 | title, 48 | style: TextStyle( 49 | fontWeight: FontWeight.w700, 50 | color: Theme.of(context).textTheme.bodyText1.color 51 | ) 52 | ), 53 | if (message != null) 54 | Text( 55 | message, 56 | style: TextStyle( 57 | color: Theme.of(context).textTheme.bodyText1.color 58 | ), 59 | maxLines: 1, 60 | overflow: TextOverflow.clip, 61 | ), 62 | SizedBox(height: 4), 63 | ], 64 | ), 65 | ), 66 | ], 67 | ), 68 | StreamBuilder( 69 | stream: screenStateStream, 70 | builder: (context, snapshot) { 71 | final screenState = snapshot.data; 72 | final state = screenState?.playbackState; 73 | final processingState = 74 | state?.processingState ?? AudioProcessingState.none; 75 | return Container( 76 | height: processingState != AudioProcessingState.none 77 | ? kToolbarHeight * 1.15 78 | : 0 79 | ); 80 | } 81 | ), 82 | ], 83 | ), 84 | duration: duration == null ? 3 : duration, 85 | shape: RoundedRectangleBorder( 86 | borderRadius: BorderRadius.only( 87 | topLeft: Radius.circular(10), 88 | topRight: Radius.circular(10) 89 | ) 90 | ), 91 | backgroundColor: Theme.of(context).canvasColor 92 | ); 93 | if (scaffoldKey == null) 94 | ScaffoldMessenger.of(context).showSnackBar(snack); 95 | else 96 | scaffoldKey.showSnackBar(snack); 97 | } 98 | } -------------------------------------------------------------------------------- /lib/screens/musicScreen/tabs/albums.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:animations/animations.dart'; 4 | import 'package:audio_service/audio_service.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:image_fade/image_fade.dart'; 7 | import 'package:songtube/internal/ffmpeg/extractor.dart'; 8 | import 'package:songtube/internal/models/mediaItemSorts.dart'; 9 | import 'package:songtube/screens/musicScreen/components/music_type_expandable.dart'; 10 | import 'package:songtube/screens/musicScreen/components/songsList.dart'; 11 | 12 | class MusicScreenAlbumsTab extends StatefulWidget { 13 | final List songs; 14 | final String searchQuery; 15 | MusicScreenAlbumsTab({ 16 | this.songs, 17 | this.searchQuery 18 | }); 19 | @override 20 | _MusicScreenAlbumsTabState createState() => _MusicScreenAlbumsTabState(); 21 | } 22 | 23 | class _MusicScreenAlbumsTabState extends State { 24 | 25 | // Albums 26 | List _albums = []; 27 | 28 | // Current album 29 | MediaItemAlbum currentAlbum; 30 | 31 | // Albums GridView Key 32 | final albumsGridKey = const PageStorageKey('albumsGrid'); 33 | 34 | @override 35 | void initState() { 36 | widget.songs.forEach((song) => songCreateOrAssignToAlbum(song)); 37 | super.initState(); 38 | } 39 | 40 | void songCreateOrAssignToAlbum(MediaItem song) { 41 | // Check if album exist and create it 42 | if (_albums.indexWhere((album) => album.albumTitle == song.album) == -1) { 43 | // Create album and add the song 44 | _albums.add( 45 | MediaItemAlbum( 46 | albumTitle: song.album, 47 | albumAuthor: song.artist, 48 | mediaItems: [] 49 | ) 50 | ); 51 | } 52 | // Add song to Album 53 | int indexToAlbum = _albums.indexWhere((album) => album.albumTitle == song.album); 54 | _albums[indexToAlbum].mediaItems.add(song); 55 | } 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | List albums = []; 60 | for (int i = 0; i < _albums.length; i++) { 61 | if (widget.searchQuery == "" || getSearchQueryMatch(_albums[i])) 62 | albums.add(_albums[i]); 63 | } 64 | return ListView.builder( 65 | key: albumsGridKey, 66 | itemCount: albums.length, 67 | itemBuilder: (context, index) { 68 | MediaItemAlbum album = albums[index]; 69 | return FutureBuilder( 70 | future: FFmpegExtractor.getAudioArtwork( 71 | audioFile: album.mediaItems[0].id, 72 | audioId: album.mediaItems[0].extras['albumId'], 73 | ), 74 | builder: (context, future) { 75 | return MusicScreenTypeExpandable( 76 | title: album.albumTitle, 77 | description: album.albumAuthor, 78 | lowResThumbnail: album.mediaItems[0].extras['artwork'], 79 | thumbnail: future.hasData ? future.data.path : null, 80 | songs: album.mediaItems, 81 | ); 82 | } 83 | ); 84 | } 85 | ); 86 | } 87 | 88 | bool getSearchQueryMatch(MediaItemAlbum album) { 89 | if (widget.searchQuery != "") { 90 | if (album.albumTitle.toLowerCase().contains(widget.searchQuery.toLowerCase())) { 91 | return true; 92 | } else if (album.albumTitle.toLowerCase().contains(widget.searchQuery.toLowerCase())) { 93 | return true; 94 | } else { 95 | return false; 96 | } 97 | } else { 98 | return true; 99 | } 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /lib/ui/components/searchHistory.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:http/http.dart' as http; 5 | import 'package:provider/provider.dart'; 6 | import 'package:songtube/provider/configurationProvider.dart'; 7 | 8 | class SearchHistoryList extends StatefulWidget { 9 | final Function(String) onItemTap; 10 | final String searchQuery; 11 | SearchHistoryList({ 12 | @required this.onItemTap, 13 | this.searchQuery = "" 14 | }); 15 | 16 | @override 17 | _SearchHistoryListState createState() => _SearchHistoryListState(); 18 | } 19 | 20 | class _SearchHistoryListState extends State { 21 | 22 | http.Client client; 23 | 24 | @override 25 | void initState() { 26 | client = http.Client(); 27 | super.initState(); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | client.close(); 33 | super.dispose(); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | ConfigurationProvider config = Provider.of(context); 39 | List searchHistory = config.getSearchHistory(); 40 | List suggestionsList = []; 41 | List finalList = []; 42 | return FutureBuilder( 43 | future: widget.searchQuery != "" ? client.get(Uri.parse( 44 | 'http://suggestqueries.google.com/complete/search?client=firefox&q=${widget.searchQuery}'), 45 | headers: { 46 | 'user-agent': 47 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 48 | '(KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36', 49 | 'accept-language': 'en-US,en;q=1.0', 50 | } 51 | ) : null, 52 | builder: (context, AsyncSnapshot suggestions) { 53 | suggestionsList.clear(); 54 | if (suggestions.hasData && widget.searchQuery != "") { 55 | var map = jsonDecode(suggestions.data.body); 56 | var mapList = map[1]; 57 | mapList.forEach((result) { 58 | suggestionsList.add(result); 59 | }); 60 | } 61 | finalList = suggestionsList + searchHistory; 62 | return ListView.builder( 63 | itemExtent: 40, 64 | itemCount: finalList.length, 65 | itemBuilder: (context, index) { 66 | String item = finalList[index]; 67 | return ListTile( 68 | title: Text( 69 | "$item", 70 | style: TextStyle( 71 | color: Theme.of(context).textTheme.bodyText1.color, 72 | fontSize: 14 73 | ), 74 | maxLines: 1, 75 | overflow: TextOverflow.fade, 76 | softWrap: false, 77 | ), 78 | leading: SizedBox( 79 | width: 40, 80 | height: 40, 81 | child: Icon( 82 | suggestionsList.contains(item) 83 | ? Icons.search 84 | : Icons.history_outlined, 85 | color: Theme.of(context).iconTheme.color 86 | ), 87 | ), 88 | trailing: !suggestionsList.contains(item) ? IconButton( 89 | icon: Icon(Icons.clear, size: 20), 90 | onPressed: () { 91 | config.removeStringfromSearchHistory(index); 92 | }, 93 | ) : null, 94 | onTap: () => widget.onItemTap(item), 95 | ); 96 | }, 97 | ); 98 | }, 99 | ); 100 | } 101 | } -------------------------------------------------------------------------------- /lib/players/components/musicPlayer/ui/playerBackground.dart: -------------------------------------------------------------------------------- 1 | // Dart 2 | import 'dart:io'; 3 | import 'dart:ui'; 4 | 5 | // Flutter 6 | import 'package:flutter/material.dart'; 7 | 8 | // Packages 9 | import 'package:image_fade/image_fade.dart'; 10 | import 'package:provider/provider.dart'; 11 | import 'package:songtube/provider/mediaProvider.dart'; 12 | import 'package:transparent_image/transparent_image.dart'; 13 | 14 | class PlayerBackground extends StatefulWidget { 15 | final File backgroundImage; 16 | final bool enableBlur; 17 | final double blurIntensity; 18 | final Widget child; 19 | final Color backdropColor; 20 | final double backdropOpacity; 21 | PlayerBackground({ 22 | @required this.backgroundImage, 23 | this.enableBlur = true, 24 | this.blurIntensity = 22.0, 25 | @required this.child, 26 | this.backdropColor = Colors.black, 27 | this.backdropOpacity = 0.4 28 | }); 29 | 30 | @override 31 | State createState() => _PlayerBackgroundState(); 32 | } 33 | 34 | class _PlayerBackgroundState extends State with TickerProviderStateMixin { 35 | 36 | AnimationController animationController; 37 | 38 | @override 39 | void initState() { 40 | animationController = AnimationController( 41 | vsync: this, duration: Duration(milliseconds: 400), value: 1); 42 | super.initState(); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | MediaProvider mediaProvider = Provider.of(context); 48 | if (animationController.status == AnimationStatus.forward) { 49 | if (mediaProvider.showLyrics) { 50 | animationController.reverse(); 51 | } 52 | } else if (animationController.status == AnimationStatus.forward) { 53 | if (!mediaProvider.showLyrics) { 54 | animationController.forward(); 55 | } 56 | } 57 | return Stack( 58 | children: [ 59 | AnimatedSwitcher( 60 | duration: Duration(milliseconds: 400), 61 | child: widget.enableBlur ? ImageFade( 62 | image: widget.backgroundImage.path.isEmpty 63 | ? MemoryImage(kTransparentImage) 64 | : FileImage(widget.backgroundImage), 65 | height: double.infinity, 66 | width: double.infinity, 67 | fit: BoxFit.cover, 68 | ) : Container( 69 | color: Theme.of(context).scaffoldBackgroundColor, 70 | ) 71 | ), 72 | AnimatedBuilder( 73 | animation: animationController, 74 | builder: (context, child) { 75 | return AnimatedContainer( 76 | duration: Duration(seconds: 1), 77 | width: MediaQuery.of(context).size.width, 78 | height: MediaQuery.of(context).size.height, 79 | color: widget.backdropColor.withOpacity(mediaProvider.showLyrics ? 0.8 : widget.backdropOpacity), 80 | child: BackdropFilter( 81 | filter: ImageFilter.blur( 82 | tileMode: TileMode.mirror, 83 | sigmaX: widget.blurIntensity * animationController.value, 84 | sigmaY: widget.blurIntensity * animationController.value, 85 | ), 86 | child: child 87 | ), 88 | ); 89 | }, 90 | child: Column( 91 | children: [ 92 | Expanded(child: widget.child), 93 | Container(height: MediaQuery.of(context).padding.bottom) 94 | ], 95 | ), 96 | ), 97 | ], 98 | ); 99 | } 100 | } -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/internal/database/databaseService.dart: -------------------------------------------------------------------------------- 1 | // Dart 2 | import 'dart:io'; 3 | 4 | // Internal 5 | import 'package:audio_tagger/audio_tagger.dart'; 6 | import 'package:songtube/internal/ffmpeg/extractor.dart'; 7 | import 'package:songtube/internal/models/songFile.dart'; 8 | 9 | // Packages 10 | import 'package:path/path.dart'; 11 | import 'package:path_provider/path_provider.dart'; 12 | import 'package:sqflite/sqflite.dart'; 13 | import 'package:string_validator/string_validator.dart'; 14 | 15 | const String table = "itemsTable"; 16 | 17 | class DatabaseService { 18 | 19 | DatabaseService._privateConstructor(); 20 | static final DatabaseService instance = DatabaseService._privateConstructor(); 21 | 22 | // only have a single app-wide reference to the database 23 | static Database _database; 24 | Future get database async { 25 | if (_database != null) return _database; 26 | // lazily instantiate the db the first time it is accessed 27 | _database = await _initDatabase(); 28 | return _database; 29 | } 30 | 31 | _initDatabase() async { 32 | Directory documentsDirectory = await getApplicationDocumentsDirectory(); 33 | String path = join(documentsDirectory.path, 'MediaItems.db'); 34 | return await openDatabase(path, 35 | version: 1, 36 | onCreate: _onCreate); 37 | } 38 | 39 | // SQL code to create the database table 40 | Future _onCreate(Database db, int version) async { 41 | await db.execute( 42 | '''CREATE TABLE $table( 43 | id INTEGER PRIMARY KEY AUTOINCREMENT, 44 | title STRING, 45 | album STRING, 46 | author STRING, 47 | duration STRING, 48 | downloadType STRING, 49 | path STRING, 50 | fileSize STRING, 51 | coverUrl STRING) 52 | ''', 53 | ); 54 | } 55 | 56 | Future insertDownload(SongFile download) async { 57 | Database db = await database; 58 | await db.insert(table, download.toMap()); 59 | } 60 | 61 | Future getDownload(String id) async { 62 | Database db = await database; 63 | List data = await db.query(table, 64 | where: 'id = ?', 65 | whereArgs: [id] 66 | ); 67 | if (data.length > 0) { 68 | return SongFile.fromMap(data.first); 69 | } 70 | return null; 71 | } 72 | 73 | Future> getDownloadList() async { 74 | List list = []; 75 | Database db = await database; 76 | var result = await db.query(table, columns: [ 77 | "id", 78 | "title", 79 | "album", 80 | "author", 81 | "duration", 82 | "downloadType", 83 | "path", 84 | "fileSize", 85 | "coverUrl" 86 | ]); 87 | await Future.forEach(result, (element) async { 88 | SongFile songFile = SongFile.fromMap(element); 89 | if (await File(songFile.path).exists()) { 90 | File coverImage = await FFmpegExtractor.getAudioArtwork( 91 | audioFile: songFile.path, 92 | extractionMethod: ArtworkExtractMethod.Automatic, 93 | forceExtraction: true 94 | ); 95 | if (!await coverImage.exists()) { 96 | if (isURL(songFile.coverUrl)) { 97 | coverImage = await AudioTagger.generateCover(songFile.coverUrl); 98 | } else { 99 | coverImage = File(songFile.coverUrl); 100 | } 101 | } 102 | songFile.coverPath = coverImage.path; 103 | list.add(songFile); 104 | } 105 | }); 106 | return list; 107 | } 108 | 109 | Future deleteDownload(int id) async { 110 | Database db = await database; 111 | return await db.delete(table, where: 'id = ?', whereArgs: [id]); 112 | } 113 | 114 | } -------------------------------------------------------------------------------- /lib/players/components/musicPlayer/ui/playerSlider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:audio_service/audio_service.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:rxdart/rxdart.dart'; 6 | 7 | class PlayerSlider extends StatelessWidget { 8 | final PlaybackState state; 9 | final MediaItem mediaItem; 10 | final Color sliderColor; 11 | final Color textColor; 12 | PlayerSlider({ 13 | @required this.state, 14 | @required this.mediaItem, 15 | @required this.sliderColor, 16 | @required this.textColor 17 | }); 18 | final BehaviorSubject _dragPositionSubject = 19 | BehaviorSubject.seeded(null); 20 | @override 21 | Widget build(BuildContext context) { 22 | return StreamBuilder( 23 | stream: Rx.combineLatest2( 24 | _dragPositionSubject.stream, 25 | Stream.periodic(Duration(milliseconds: 1000)), 26 | (dragPosition, _) => dragPosition), 27 | builder: (context, snapshot) { 28 | Duration position = state?.currentPosition ?? Duration.zero; 29 | Duration duration = mediaItem?.duration ?? Duration(seconds: 1); 30 | return duration != null 31 | ? Column( 32 | children: [ 33 | Padding( 34 | padding: const EdgeInsets.only(left: 8, right: 8), 35 | child: SliderTheme( 36 | data: SliderTheme.of(context).copyWith( 37 | thumbShape: RoundSliderThumbShape(enabledThumbRadius: 0), 38 | overlayShape: RoundSliderOverlayShape(overlayRadius: 10), 39 | valueIndicatorTextStyle: TextStyle( 40 | color: sliderColor, 41 | ), 42 | trackHeight: 2, 43 | ), 44 | child: Slider( 45 | activeColor: sliderColor.withOpacity(0.7), 46 | inactiveColor: Colors.black12.withOpacity(0.1), 47 | min: 0.0, 48 | max: duration.inMilliseconds?.toDouble(), 49 | value: max(0.0, min( 50 | position.inMilliseconds.toDouble(), 51 | duration.inMilliseconds?.toDouble() 52 | )), 53 | onChanged: (value) { 54 | _dragPositionSubject.add(value); 55 | }, 56 | onChangeEnd: (value) { 57 | AudioService.seekTo(Duration(milliseconds: value.toInt())); 58 | _dragPositionSubject.add(null); 59 | }, 60 | ) 61 | ), 62 | ), 63 | Padding( 64 | padding: EdgeInsets.only(left: 20, right: 20), 65 | child: Row( 66 | children: [ 67 | Text( 68 | "${position.inMinutes}:${(position.inSeconds.remainder(60).toString().padLeft(2, '0'))}", 69 | style: TextStyle( 70 | fontFamily: "YTSans", 71 | fontSize: 12, 72 | color: textColor.withOpacity(0.6) 73 | ), 74 | ), 75 | Spacer(), 76 | Text( 77 | "${duration.inMinutes}:${(duration.inSeconds.remainder(60).toString().padLeft(2, '0'))}", 78 | style: TextStyle( 79 | fontFamily: "YTSans", 80 | fontSize: 12, 81 | color: textColor.withOpacity(0.6) 82 | ), 83 | ) 84 | ], 85 | ), 86 | ) 87 | ], 88 | ) 89 | : Container(); 90 | }, 91 | ); 92 | } 93 | } -------------------------------------------------------------------------------- /lib/players/components/youtubePlayer/ui/engagement.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intl/intl.dart'; 3 | import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; 4 | import 'package:songtube/internal/languages.dart'; 5 | 6 | class VideoEngagement extends StatelessWidget { 7 | final int likeCount; 8 | final int dislikeCount; 9 | final int viewCount; 10 | final Function onSaveToPlaylist; 11 | final Function onDownload; 12 | final Function onShare; 13 | VideoEngagement({ 14 | @required this.likeCount, 15 | @required this.dislikeCount, 16 | @required this.viewCount, 17 | @required this.onSaveToPlaylist, 18 | @required this.onDownload, 19 | @required this.onShare, 20 | }); 21 | @override 22 | Widget build(BuildContext context) { 23 | return Row( 24 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 25 | crossAxisAlignment: CrossAxisAlignment.center, 26 | children: [ 27 | // Likes Counter 28 | _engagementTile( 29 | icon: Icon(MdiIcons.thumbUpOutline, color: Theme.of(context).iconTheme.color), 30 | text: Text( 31 | likeCount != -1 32 | ? NumberFormat.compact().format(likeCount) : 'Likes', 33 | style: TextStyle( 34 | fontSize: 10, 35 | fontFamily: 'Product Sans', 36 | ), 37 | ), 38 | ), 39 | // Dislikes Counter 40 | _engagementTile( 41 | icon: Icon(MdiIcons.thumbDownOutline, color: Theme.of(context).iconTheme.color), 42 | text: Text( 43 | dislikeCount != -1 44 | ? NumberFormat.compact().format(dislikeCount) : 'Dislikes', 45 | style: TextStyle( 46 | fontSize: 10, 47 | fontFamily: 'Product Sans', 48 | ), 49 | ), 50 | ), 51 | // 52 | _engagementTile( 53 | icon: Icon(MdiIcons.shareOutline, color: Theme.of(context).iconTheme.color), 54 | text: Text( 55 | Languages.of(context).labelShare, 56 | style: TextStyle( 57 | fontSize: 10 58 | ), 59 | ), 60 | onPressed: onShare 61 | ), 62 | // Add to Playlist Button 63 | _engagementTile( 64 | icon: Icon(MdiIcons.playlistPlus, color: Theme.of(context).iconTheme.color), 65 | text: Text( 66 | Languages.of(context).labelPlaylist, 67 | style: TextStyle( 68 | fontSize: 10, 69 | fontFamily: 'Product Sans', 70 | ), 71 | ), 72 | onPressed: onSaveToPlaylist 73 | ), 74 | // Open Comments Button 75 | _engagementTile( 76 | icon: Icon(MdiIcons.downloadOutline, color: Theme.of(context).iconTheme.color), 77 | text: Text( 78 | Languages.of(context).labelDownload, 79 | style: TextStyle( 80 | fontSize: 10, 81 | fontFamily: 'Product Sans', 82 | ), 83 | ), 84 | onPressed: onDownload 85 | ), 86 | ], 87 | ); 88 | } 89 | 90 | Widget _engagementTile({ 91 | final Widget icon, 92 | final Widget text, 93 | final Function onPressed 94 | }) { 95 | return Container( 96 | width: 65, 97 | height: 65, 98 | child: InkWell( 99 | borderRadius: BorderRadius.circular(25), 100 | onTap: onPressed, 101 | child: Column( 102 | mainAxisAlignment: MainAxisAlignment.center, 103 | crossAxisAlignment: CrossAxisAlignment.center, 104 | children: [ 105 | icon, 106 | SizedBox(height: 2), 107 | text 108 | ], 109 | ), 110 | ), 111 | ); 112 | } 113 | 114 | } --------------------------------------------------------------------------------