From 4e5a3ca64c7e24d9f51cbd3b3ae606be8bbaec00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20=C4=90=E1=BB=A9c=20Tu=E1=BA=A5n=20Minh?= Date: Tue, 1 Aug 2023 13:36:10 +0700 Subject: [PATCH] Fixed maxrave-dev/SimpMusic#4 Fixed bugs Migrate Gradle to Kotlin KTS New Feature: - Remove Thumbnail Cache - Move or remove Queue Track - Backup and restore data (maxrave-dev/SimpMusic#2) - Add swipe to remove miniplayer - Add SimpMusic to YouTube, YouTube Music's share menu (maxrave-dev/SimpMusic#9) - Open YouTube link by default - Add static shortcuts - Auto save lyrics for offline playback - Change Quality --- app/build.gradle | 150 -- app/build.gradle.kts | 146 ++ app/proguard-rules.pro | 2 +- app/src/main/AndroidManifest.xml | 82 +- .../simpmusic/adapter/queue/QueueAdapter.kt | 14 +- .../com/maxrave/simpmusic/common/Config.kt | 10 +- .../data/api/search/RemoteDataSource.kt | 7 +- .../data/api/search/SearchService.kt | 9 + .../data/dataStore/DataStoreManager.kt | 28 +- .../maxrave/simpmusic/data/db/Converters.kt | 13 + .../maxrave/simpmusic/data/db/DatabaseDao.kt | 19 + .../simpmusic/data/db/LocalDataSource.kt | 5 + .../simpmusic/data/db/MusicDatabase.kt | 3 +- .../data/db/entities/LyricsEntity.kt | 17 + .../data/model/browse/artist/ChannelId.kt | 9 + .../simpmusic/data/model/songfull/SongFull.kt | 18 + .../data/repository/MainRepository.kt | 9 + .../simpmusic/di/LocalServiceModule.kt | 3 +- .../simpmusic/di/MusicServiceModule.kt | 5 +- .../com/maxrave/simpmusic/extension/AllExt.kt | 45 + .../service/SimpleMediaNotificationManager.kt | 11 +- .../simpmusic/service/SimpleMediaService.kt | 5 +- .../service/SimpleMediaServiceHandler.kt | 4 + .../test/download/MusicDownloadService.kt | 2 +- .../service/test/source/MusicSource.kt | 89 +- .../com/maxrave/simpmusic/ui/MainActivity.kt | 195 +- .../simpmusic/ui/fragment/SearchFragment.kt | 54 +- .../ui/fragment/home/HomeFragment.kt | 10 +- .../ui/fragment/home/SettingsFragment.kt | 80 + .../ui/fragment/library/DownloadedFragment.kt | 38 + .../ui/fragment/library/LibraryFragment.kt | 1 - .../fragment/other/LocalPlaylistFragment.kt | 4 +- .../ui/fragment/player/NowPlayingFragment.kt | 65 +- .../ui/fragment/player/QueueFragment.kt | 43 + .../viewModel/DownloadedViewModel.kt | 35 + .../simpmusic/viewModel/LibraryViewModel.kt | 1 + .../simpmusic/viewModel/SettingsViewModel.kt | 112 ++ .../simpmusic/viewModel/SharedViewModel.kt | 138 +- .../main/res/drawable/baseline_album_24.xml | 5 + ...baseline_keyboard_double_arrow_down_24.xml | 6 + .../baseline_keyboard_double_arrow_up_24.xml | 6 + .../res/drawable/baseline_music_note_24.xml | 5 + .../main/res/drawable/baseline_search_24.xml | 5 + .../main/res/drawable/logo_simpmusic_01.xml | 1630 +++++++++++++++++ .../logo_simpmusic_01_removebg_preview.xml | 1025 +++++++++++ app/src/main/res/layout/activity_main.xml | 253 +-- .../bottom_sheet_queue_track_option.xml | 118 ++ .../main/res/layout/fragment_now_playing.xml | 5 +- app/src/main/res/layout/fragment_search.xml | 44 +- app/src/main/res/layout/fragment_settings.xml | 131 +- app/src/main/res/layout/item_queue_track.xml | 18 +- .../main/res/layout/queue_bottom_sheet.xml | 5 - .../res/navigation/nav_bottom_navigation.xml | 21 + app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 9 + app/src/main/res/xml/provider_paths.xml | 12 + app/src/main/res/xml/shortcuts.xml | 33 + build.gradle | 8 - build.gradle.kts | 8 + .../metadata/android/en-US/changelogs/2.txt | 17 + .../android/en-US/full_description.txt | 1 + gradle.properties | 2 +- settings.gradle => settings.gradle.kts | 4 +- 63 files changed, 4385 insertions(+), 469 deletions(-) delete mode 100644 app/build.gradle create mode 100644 app/build.gradle.kts create mode 100644 app/src/main/java/com/maxrave/simpmusic/data/db/entities/LyricsEntity.kt create mode 100644 app/src/main/java/com/maxrave/simpmusic/data/model/browse/artist/ChannelId.kt create mode 100644 app/src/main/java/com/maxrave/simpmusic/data/model/songfull/SongFull.kt create mode 100644 app/src/main/res/drawable/baseline_album_24.xml create mode 100644 app/src/main/res/drawable/baseline_keyboard_double_arrow_down_24.xml create mode 100644 app/src/main/res/drawable/baseline_keyboard_double_arrow_up_24.xml create mode 100644 app/src/main/res/drawable/baseline_music_note_24.xml create mode 100644 app/src/main/res/drawable/baseline_search_24.xml create mode 100644 app/src/main/res/drawable/logo_simpmusic_01.xml create mode 100644 app/src/main/res/drawable/logo_simpmusic_01_removebg_preview.xml create mode 100644 app/src/main/res/layout/bottom_sheet_queue_track_option.xml create mode 100644 app/src/main/res/xml/provider_paths.xml create mode 100644 app/src/main/res/xml/shortcuts.xml delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 fastlane/metadata/android/en-US/changelogs/2.txt rename settings.gradle => settings.gradle.kts (82%) diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 35038a5d..00000000 --- a/app/build.gradle +++ /dev/null @@ -1,150 +0,0 @@ -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' - id 'androidx.navigation.safeargs' - id 'kotlin-kapt' - id 'com.google.dagger.hilt.android' -} - -android { - namespace 'com.maxrave.simpmusic' - compileSdk 34 - - defaultConfig { - applicationId "com.maxrave.simpmusic" - minSdk 26 - targetSdk 34 - versionCode 1 - versionName "0.0.1-beta" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - //enable view binding - viewBinding { - enabled = true - } - packagingOptions { - jniLibs { - useLegacyPackaging true - } - } -} - -dependencies { - -// def youtube_extractor_version = '0.0.7' -// implementation ("com.github.maxrave-dev:kotlin-youtubeExtractor:$youtube_extractor_version") {changing = true} - - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' - implementation 'androidx.core:core-ktx:1.10.1' - implementation 'androidx.appcompat:appcompat:1.6.1' - //material design 3 - implementation 'com.google.android.material:material:1.9.0' - //runtime - implementation "androidx.startup:startup-runtime:1.1.1" - //ExoPlayer - def media3_version = '1.1.0' - - implementation "androidx.media3:media3-exoplayer:$media3_version" - implementation "androidx.media3:media3-ui:$media3_version" - implementation "androidx.media3:media3-session:$media3_version" - implementation "androidx.media3:media3-exoplayer-dash:$media3_version" - implementation "androidx.media3:media3-exoplayer-hls:$media3_version" - implementation "androidx.media3:media3-exoplayer-rtsp:$media3_version" - implementation "androidx.media3:media3-exoplayer-smoothstreaming:$media3_version" - implementation "androidx.media3:media3-exoplayer-workmanager:$media3_version" - implementation "androidx.media3:media3-datasource-okhttp:$media3_version" - - //palette color - implementation 'androidx.palette:palette-ktx:1.0.0' - //expandable text view - implementation 'com.github.giangpham96:expandable-text:2.0.0' - - - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - - implementation 'androidx.room:room-runtime:2.5.2' - implementation 'androidx.room:room-ktx:2.5.2' - kapt "androidx.room:room-compiler:2.5.2" - //Legacy Support - implementation 'androidx.legacy:legacy-support-v4:1.0.0' - //Coroutines - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2' - //Navigation - implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0' - implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' - - //Retrofit 2 - implementation 'com.squareup.retrofit2:retrofit:2.9.0' - implementation 'com.squareup.retrofit2:converter-gson:2.9.0' - implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0' - //OkHttp - implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11' - - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - - //Coil - implementation 'io.coil-kt:coil:2.4.0' - //Glide - implementation 'com.github.bumptech.glide:glide:4.15.1' - //Easy Permissions - implementation 'pub.devrel:easypermissions:3.0.0' - //Palette Color - implementation 'androidx.palette:palette-ktx:1.0.0' - - //Preference - implementation 'androidx.preference:preference-ktx:1.2.0' - - //fragment ktx - implementation 'androidx.fragment:fragment-ktx:1.6.0' - //Hilt - implementation 'com.google.dagger:hilt-android:2.47' - kapt 'com.google.dagger:hilt-compiler:2.47' - //Preference ktx - implementation 'androidx.preference:preference-ktx:1.2.0' - //DataStore - implementation "androidx.datastore:datastore-preferences:1.0.0" - //Swipe To Refresh - implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01" - //Insetter - implementation 'dev.chrisbanes.insetter:insetter:0.6.1' - implementation 'dev.chrisbanes.insetter:insetter-dbx:0.6.1' - - //Shimmer - implementation 'com.facebook.shimmer:shimmer:0.5.0' - - //Lottie - def lottieVersion = '6.1.0' - implementation "com.airbnb.android:lottie:$lottieVersion" - - //Paging 3 - def paging_version = "3.2.0-rc01" - implementation "androidx.paging:paging-runtime-ktx:$paging_version" - - -} -// Allow references to generated code -kapt { - correctErrorTypes true -} -hilt { - enableAggregatingTask = true -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..3c140ba3 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,146 @@ +plugins { + id ("com.android.application") + id ("org.jetbrains.kotlin.android") + id ("androidx.navigation.safeargs") + id ("kotlin-kapt") + id ("com.google.dagger.hilt.android") +} + +android { + namespace = "com.maxrave.simpmusic" + compileSdk = 34 + + defaultConfig { + applicationId = "com.maxrave.simpmusic" + minSdk = 26 + targetSdk = 34 + versionCode = 2 + versionName = "0.0.2-beta" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles (getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + //enable view binding + buildFeatures { + viewBinding = true + } + packaging { + jniLibs.useLegacyPackaging = true + } +} + +dependencies { + + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") + implementation("androidx.core:core-ktx:1.10.1") + implementation("androidx.appcompat:appcompat:1.6.1") + //material design3 + implementation("com.google.android.material:material:1.9.0") + //runtime + implementation("androidx.startup:startup-runtime:1.1.1") + //ExoPlayer + val media3_version= "1.1.0" + + implementation("androidx.media3:media3-exoplayer:$media3_version") + implementation("androidx.media3:media3-ui:$media3_version") + implementation("androidx.media3:media3-session:$media3_version") + implementation("androidx.media3:media3-exoplayer-dash:$media3_version") + implementation("androidx.media3:media3-exoplayer-hls:$media3_version") + implementation("androidx.media3:media3-exoplayer-rtsp:$media3_version") + implementation("androidx.media3:media3-exoplayer-smoothstreaming:$media3_version") + implementation("androidx.media3:media3-exoplayer-workmanager:$media3_version") + implementation("androidx.media3:media3-datasource-okhttp:$media3_version") + + //palette color + implementation("androidx.palette:palette-ktx:1.0.0") + //expandable text view + implementation("com.github.giangpham96:expandable-text:2.0.0") + + + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + implementation("androidx.room:room-runtime:2.5.2") + implementation("androidx.room:room-ktx:2.5.2") + kapt ("androidx.room:room-compiler:2.5.2") + //Legacy Support + implementation("androidx.legacy:legacy-support-v4:1.0.0") + //Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + //Navigation + implementation("androidx.navigation:navigation-fragment-ktx:2.6.0") + implementation("androidx.navigation:navigation-ui-ktx:2.6.0") + + //Retrofit 2 + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + //OkHttp + implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11") + + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + + //Coil + implementation("io.coil-kt:coil:2.4.0") + //Glide + implementation("com.github.bumptech.glide:glide:4.15.1") + //Easy Permissions + implementation("pub.devrel:easypermissions:3.0.0") + //Palette Color + implementation("androidx.palette:palette-ktx:1.0.0") + + //Preference + implementation("androidx.preference:preference-ktx:1.2.0") + + //fragment ktx + implementation("androidx.fragment:fragment-ktx:1.6.0") + //Hilt + implementation("com.google.dagger:hilt-android:2.47") + kapt ("com.google.dagger:hilt-compiler:2.47") + //Preference ktx + implementation("androidx.preference:preference-ktx:1.2.0") + //DataStore + implementation("androidx.datastore:datastore-preferences:1.0.0") + //Swipe To Refresh + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") + //Insetter + implementation("dev.chrisbanes.insetter:insetter:0.6.1") + implementation("dev.chrisbanes.insetter:insetter-dbx:0.6.1") + + //Shimmer + implementation("com.facebook.shimmer:shimmer:0.5.0") + + //Lottie + val lottieVersion = "6.1.0" + implementation("com.airbnb.android:lottie:$lottieVersion") + + //Paging 3 + val paging_version= "3.2.0-rc01" + implementation("androidx.paging:paging-runtime-ktx:$paging_version") + + implementation("androidx.legacy:legacy-support-v4:1.0.0") + implementation("com.daimajia.swipelayout:library:1.2.0@aar") + +} +// Allow references to generated code +kapt { + correctErrorTypes = true +} +hilt { + enableAggregatingTask = true +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb434..ff59496d 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fbd46c7f..db90d0c3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,23 +13,88 @@ + android:theme="@style/Theme.SimpMusic"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/maxrave/simpmusic/adapter/queue/QueueAdapter.kt b/app/src/main/java/com/maxrave/simpmusic/adapter/queue/QueueAdapter.kt index 789a4625..2753d1a1 100644 --- a/app/src/main/java/com/maxrave/simpmusic/adapter/queue/QueueAdapter.kt +++ b/app/src/main/java/com/maxrave/simpmusic/adapter/queue/QueueAdapter.kt @@ -16,22 +16,32 @@ import com.maxrave.simpmusic.extension.toListName class QueueAdapter(private val listTrack: ArrayList, val context: Context, private var currentPlaying: Int): RecyclerView.Adapter() { private lateinit var mListener: OnItemClickListener + private lateinit var mOptionListener: OnOptionClickListener interface OnItemClickListener{ fun onItemClick(position: Int) } fun setOnClickListener(listener: OnItemClickListener){ mListener = listener } + interface OnOptionClickListener{ + fun onOptionClick(position: Int) + } + fun setOnOptionClickListener(listener: OnOptionClickListener){ + mOptionListener = listener + } fun setCurrentPlaying(position: Int) { currentPlaying = position notifyDataSetChanged() } - inner class QueueViewHolder(val binding: ItemQueueTrackBinding, listener: OnItemClickListener): RecyclerView.ViewHolder(binding.root) { + inner class QueueViewHolder(val binding: ItemQueueTrackBinding, listener: OnItemClickListener, optionClickListener: OnOptionClickListener): RecyclerView.ViewHolder(binding.root) { init { binding.root.setOnClickListener { listener.onItemClick(bindingAdapterPosition) } + binding.btMore.setOnClickListener { + optionClickListener.onOptionClick(bindingAdapterPosition) + } } } @@ -46,7 +56,7 @@ class QueueAdapter(private val listTrack: ArrayList, val context: Context } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QueueViewHolder { - return QueueViewHolder(ItemQueueTrackBinding.inflate(LayoutInflater.from(parent.context), parent, false), mListener) + return QueueViewHolder(ItemQueueTrackBinding.inflate(LayoutInflater.from(parent.context), parent, false), mListener, mOptionListener) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/maxrave/simpmusic/common/Config.kt b/app/src/main/java/com/maxrave/simpmusic/common/Config.kt index 412e484e..563af68a 100644 --- a/app/src/main/java/com/maxrave/simpmusic/common/Config.kt +++ b/app/src/main/java/com/maxrave/simpmusic/common/Config.kt @@ -39,4 +39,12 @@ object SUPPORTED_LOCATION { "MK", "MT", "MX", "MY", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PR", "PT", "PY", "QA", "RO", "RS", "RU", "SA", "SE", "SG", "SI", "SK", "SN", "SV", "TH", "TN", "TR", "TW", "TZ", "UA", "UG", "US", "UY", "VE", "VN", "YE", "ZA", "ZW") -} \ No newline at end of file +} +object QUALITY { + val items: Array = arrayOf("Low - 66kps", "High - 129kps") + val itags: Array = arrayOf(250, 251) +} + +const val SETTINGS_FILENAME = "settings" + +const val DB_NAME = "Music Database" \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/api/search/RemoteDataSource.kt b/app/src/main/java/com/maxrave/simpmusic/data/api/search/RemoteDataSource.kt index 0a236d2f..a5ded8df 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/api/search/RemoteDataSource.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/api/search/RemoteDataSource.kt @@ -1,16 +1,16 @@ package com.maxrave.simpmusic.data.api.search -import android.provider.MediaStore.Video import com.maxrave.simpmusic.common.Config import com.maxrave.simpmusic.data.dataStore.DataStoreManager -import com.maxrave.simpmusic.data.model.home.chart.Chart import com.maxrave.simpmusic.data.model.browse.album.AlbumBrowse import com.maxrave.simpmusic.data.model.browse.album.Track import com.maxrave.simpmusic.data.model.browse.artist.ArtistBrowse +import com.maxrave.simpmusic.data.model.browse.artist.ChannelId import com.maxrave.simpmusic.data.model.browse.playlist.PlaylistBrowse import com.maxrave.simpmusic.data.model.explore.mood.Mood import com.maxrave.simpmusic.data.model.explore.mood.genre.GenreObject import com.maxrave.simpmusic.data.model.explore.mood.moodmoments.MoodsMomentObject +import com.maxrave.simpmusic.data.model.home.chart.Chart import com.maxrave.simpmusic.data.model.home.homeItem import com.maxrave.simpmusic.data.model.metadata.Lyrics import com.maxrave.simpmusic.data.model.metadata.MetadataSong @@ -19,6 +19,7 @@ import com.maxrave.simpmusic.data.model.searchResult.artists.ArtistsResult import com.maxrave.simpmusic.data.model.searchResult.playlists.PlaylistsResult import com.maxrave.simpmusic.data.model.searchResult.songs.SongsResult import com.maxrave.simpmusic.data.model.searchResult.videos.VideosResult +import com.maxrave.simpmusic.data.model.songfull.SongFull import com.maxrave.simpmusic.data.model.streams.Streams import com.maxrave.simpmusic.data.model.thumbnailUrl import retrofit2.Response @@ -26,6 +27,7 @@ import javax.inject.Inject class RemoteDataSource @Inject constructor(private val searchService: SearchService, private val dataStoreManager: DataStoreManager) { suspend fun getSong(videoId: String): Response> = searchService.getSong(videoId) + suspend fun getSongFull(videoId: String): Response = searchService.getSongFull(videoId) suspend fun getThumbnails(songId: String): Response> = searchService.getThumbnails(songId) suspend fun searchAll(query: String, regionCode: String): Response> = searchService.searchAll(query, regionCode) @@ -82,4 +84,5 @@ class RemoteDataSource @Inject constructor(private val searchService: SearchServ suspend fun getRelated(videoId: String, regionCode: String): Response> = searchService.songsRelated(videoId, regionCode) suspend fun getVideoRelated(videoId: String, regionCode: String): Response> = searchService.videosRelated(videoId, regionCode) + suspend fun convertNameToId(name: String): Response = searchService.convertNameToId(name) } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/api/search/SearchService.kt b/app/src/main/java/com/maxrave/simpmusic/data/api/search/SearchService.kt index e1d70243..da3c158b 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/api/search/SearchService.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/api/search/SearchService.kt @@ -4,6 +4,7 @@ import com.maxrave.simpmusic.data.model.home.chart.Chart import com.maxrave.simpmusic.data.model.browse.album.AlbumBrowse import com.maxrave.simpmusic.data.model.browse.album.Track import com.maxrave.simpmusic.data.model.browse.artist.ArtistBrowse +import com.maxrave.simpmusic.data.model.browse.artist.ChannelId import com.maxrave.simpmusic.data.model.browse.playlist.PlaylistBrowse import com.maxrave.simpmusic.data.model.explore.mood.Mood import com.maxrave.simpmusic.data.model.explore.mood.genre.GenreObject @@ -16,6 +17,7 @@ import com.maxrave.simpmusic.data.model.searchResult.artists.ArtistsResult import com.maxrave.simpmusic.data.model.searchResult.playlists.PlaylistsResult import com.maxrave.simpmusic.data.model.searchResult.songs.SongsResult import com.maxrave.simpmusic.data.model.searchResult.videos.VideosResult +import com.maxrave.simpmusic.data.model.songfull.SongFull import com.maxrave.simpmusic.data.model.streams.Streams import com.maxrave.simpmusic.data.model.thumbnailUrl import retrofit2.Response @@ -29,6 +31,9 @@ interface SearchService { //song @GET("song") suspend fun getSong(@Query("videoId") videoId: String): Response> + + @GET("song/full/") + suspend fun getSongFull(@Query("videoId") videoId: String): Response //search @GET("search") suspend fun searchAll(@Query("q") query: String, @Query("r") region: String): Response> @@ -89,4 +94,8 @@ interface SearchService { @GET("/songs/lyrics") suspend fun getLyrics(@Query("q") query: String, @Query("r") region: String): Response + //Convert name to id + @GET("/name/") + suspend fun convertNameToId(@Query("n") query: String): Response + } diff --git a/app/src/main/java/com/maxrave/simpmusic/data/dataStore/DataStoreManager.kt b/app/src/main/java/com/maxrave/simpmusic/data/dataStore/DataStoreManager.kt index cbf468da..b820d7ca 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/dataStore/DataStoreManager.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/dataStore/DataStoreManager.kt @@ -4,6 +4,8 @@ import android.content.Context import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import com.maxrave.simpmusic.common.QUALITY as COMMON_QUALITY +import com.maxrave.simpmusic.common.SETTINGS_FILENAME import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -12,7 +14,7 @@ import javax.inject.Singleton @Singleton class DataStoreManager @Inject constructor(@ApplicationContext appContext: Context) { - private val Context.dataStore by preferencesDataStore("settings") + private val Context.dataStore by preferencesDataStore(SETTINGS_FILENAME) private val settingsDataStore = appContext.dataStore @@ -26,7 +28,31 @@ class DataStoreManager @Inject constructor(@ApplicationContext appContext: Conte } } + val quality: Flow = settingsDataStore.data.map { preferences -> + preferences[QUALITY] ?: COMMON_QUALITY.items[0].toString() + } + + suspend fun restore(isRestoring: Boolean) { + settingsDataStore.edit { settings -> + settings[IS_RESTORING_DATABASE] = if (isRestoring) TRUE else FALSE + } + } + + suspend fun setQuality(quality: String) { + settingsDataStore.edit { settings -> + settings[QUALITY] = quality + } + } + + val isRestoringDatabase: Flow = settingsDataStore.data.map { preferences -> + preferences[IS_RESTORING_DATABASE] ?: FALSE + } + companion object Settings { val LOCATION = stringPreferencesKey("location") + val QUALITY = stringPreferencesKey("quality") + val IS_RESTORING_DATABASE = stringPreferencesKey("is_restoring_database") + val TRUE = "TRUE" + val FALSE = "FALSE" } } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/Converters.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/Converters.kt index 3094d998..70d25afd 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/db/Converters.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/Converters.kt @@ -3,6 +3,7 @@ package com.maxrave.simpmusic.data.db import androidx.room.TypeConverter import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import com.maxrave.simpmusic.data.model.metadata.Line import java.lang.reflect.Type import java.time.Instant import java.time.LocalDateTime @@ -22,6 +23,18 @@ class Converters { return gson.toJson(list) } + @TypeConverter + fun fromListLineToString(list: List?): String? { + val gson = Gson() + return gson.toJson(list) + } + + @TypeConverter + fun fromStringToListLine(value: String?): List? { + val listType: Type = object : TypeToken?>() {}.type + return Gson().fromJson(value, listType) + } + @TypeConverter fun fromTimestamp(value: Long?): LocalDateTime? = if (value != null) LocalDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneOffset.UTC) diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/DatabaseDao.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/DatabaseDao.kt index 7afaab83..fbd00801 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/db/DatabaseDao.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/DatabaseDao.kt @@ -5,13 +5,17 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Transaction +import androidx.sqlite.db.SupportSQLiteQuery import com.maxrave.simpmusic.data.db.entities.AlbumEntity import com.maxrave.simpmusic.data.db.entities.ArtistEntity import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity +import com.maxrave.simpmusic.data.db.entities.LyricsEntity import com.maxrave.simpmusic.data.db.entities.PlaylistEntity import com.maxrave.simpmusic.data.db.entities.SearchHistory import com.maxrave.simpmusic.data.db.entities.SongEntity +import com.maxrave.simpmusic.extension.toSQLiteQuery import java.time.LocalDateTime @Dao @@ -121,6 +125,9 @@ interface DatabaseDao { @Query("SELECT * FROM song WHERE downloadState = 3") suspend fun getDownloadedSongs(): List + @Query("SELECT * FROM song WHERE downloadState = 1 OR downloadState = 2") + suspend fun getDownloadingSongs(): List + @Query("SELECT * FROM song WHERE videoId IN (:primaryKeyList)") fun getSongByListVideoId(primaryKeyList: List): List @@ -224,4 +231,16 @@ interface DatabaseDao { @Query("SELECT * FROM local_playlist WHERE downloadState = 3") suspend fun getDownloadedLocalPlaylists(): List + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertLyrics(lyrics: LyricsEntity) + + @Query("SELECT * FROM lyrics WHERE videoId = :videoId") + suspend fun getLyrics(videoId: String): LyricsEntity? + + @RawQuery + fun raw(supportSQLiteQuery: SupportSQLiteQuery): Int + + fun checkpoint() { + raw("pragma wal_checkpoint(full)".toSQLiteQuery()) + } } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/LocalDataSource.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/LocalDataSource.kt index c5f704c4..ca0e5034 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/db/LocalDataSource.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/LocalDataSource.kt @@ -3,6 +3,7 @@ package com.maxrave.simpmusic.data.db import com.maxrave.simpmusic.data.db.entities.AlbumEntity import com.maxrave.simpmusic.data.db.entities.ArtistEntity import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity +import com.maxrave.simpmusic.data.db.entities.LyricsEntity import com.maxrave.simpmusic.data.db.entities.PlaylistEntity import com.maxrave.simpmusic.data.db.entities.SearchHistory import com.maxrave.simpmusic.data.db.entities.SongEntity @@ -23,6 +24,7 @@ class LocalDataSource @Inject constructor(private val databaseDao: DatabaseDao) suspend fun getRecentSongs(limit: Int, offset: Int) = databaseDao.getRecentSongs(limit, offset) suspend fun getSongByListVideoId(primaryKeyList: List) = databaseDao.getSongByListVideoId(primaryKeyList) suspend fun getDownloadedSongs() = databaseDao.getDownloadedSongs() + suspend fun getDownloadingSongs() = databaseDao.getDownloadingSongs() suspend fun getLikedSongs() = databaseDao.getLikedSongs() suspend fun getLibrarySongs() = databaseDao.getLibrarySongs() suspend fun getSong(videoId: String) = databaseDao.getSong(videoId) @@ -66,4 +68,7 @@ class LocalDataSource @Inject constructor(private val databaseDao: DatabaseDao) suspend fun updateLocalPlaylistInLibrary(inLibrary: LocalDateTime, id: Long) = databaseDao.updateLocalPlaylistInLibrary(inLibrary, id) suspend fun updateLocalPlaylistDownloadState(downloadState: Int, id: Long) = databaseDao.updateLocalPlaylistDownloadState(downloadState, id) suspend fun getDownloadedLocalPlaylists() = databaseDao.getDownloadedLocalPlaylists() + + suspend fun getSavedLyrics(videoId: String) = databaseDao.getLyrics(videoId) + suspend fun insertLyrics(lyrics: LyricsEntity) = databaseDao.insertLyrics(lyrics) } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/MusicDatabase.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/MusicDatabase.kt index 5ea5918f..9e184caf 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/db/MusicDatabase.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/MusicDatabase.kt @@ -6,11 +6,12 @@ import androidx.room.TypeConverters import com.maxrave.simpmusic.data.db.entities.AlbumEntity import com.maxrave.simpmusic.data.db.entities.ArtistEntity import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity +import com.maxrave.simpmusic.data.db.entities.LyricsEntity import com.maxrave.simpmusic.data.db.entities.PlaylistEntity import com.maxrave.simpmusic.data.db.entities.SearchHistory import com.maxrave.simpmusic.data.db.entities.SongEntity -@Database(entities = [SearchHistory::class, SongEntity::class, ArtistEntity::class, AlbumEntity::class, PlaylistEntity::class, LocalPlaylistEntity::class], version = 1, exportSchema = false) +@Database(entities = [SearchHistory::class, SongEntity::class, ArtistEntity::class, AlbumEntity::class, PlaylistEntity::class, LocalPlaylistEntity::class, LyricsEntity::class], version = 1, exportSchema = false) @TypeConverters(Converters::class) abstract class MusicDatabase: RoomDatabase() { abstract fun getDatabaseDao(): DatabaseDao diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/entities/LyricsEntity.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/entities/LyricsEntity.kt new file mode 100644 index 00000000..0d61bc67 --- /dev/null +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/entities/LyricsEntity.kt @@ -0,0 +1,17 @@ +package com.maxrave.simpmusic.data.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.google.gson.annotations.SerializedName +import com.maxrave.simpmusic.data.model.metadata.Line + +@Entity(tableName = "lyrics") +data class LyricsEntity ( + @PrimaryKey(autoGenerate = false) val videoId: String, + @SerializedName("error") + val error: Boolean, + @SerializedName("lines") + val lines: List?, + @SerializedName("syncType") + val syncType: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/model/browse/artist/ChannelId.kt b/app/src/main/java/com/maxrave/simpmusic/data/model/browse/artist/ChannelId.kt new file mode 100644 index 00000000..9820334f --- /dev/null +++ b/app/src/main/java/com/maxrave/simpmusic/data/model/browse/artist/ChannelId.kt @@ -0,0 +1,9 @@ +package com.maxrave.simpmusic.data.model.browse.artist + + +import com.google.gson.annotations.SerializedName + +data class ChannelId( + @SerializedName("id") + val id: String +) \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/model/songfull/SongFull.kt b/app/src/main/java/com/maxrave/simpmusic/data/model/songfull/SongFull.kt new file mode 100644 index 00000000..78ec508c --- /dev/null +++ b/app/src/main/java/com/maxrave/simpmusic/data/model/songfull/SongFull.kt @@ -0,0 +1,18 @@ +package com.maxrave.simpmusic.data.model.songfull + + +import com.google.gson.annotations.SerializedName +import com.maxrave.simpmusic.data.model.searchResult.songs.Artist +import com.maxrave.simpmusic.data.model.searchResult.songs.Thumbnail +import com.maxrave.simpmusic.data.model.streams.Streams + +data class SongFull( + @SerializedName("artist") + val artist: List, + @SerializedName("audioStreams") + val audioStreams: List, + @SerializedName("thumbnails") + val thumbnails: List, + @SerializedName("title") + val title: String +) \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt index 5cd6c23d..96b49e9d 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt @@ -6,12 +6,14 @@ import com.maxrave.simpmusic.data.db.LocalDataSource import com.maxrave.simpmusic.data.db.entities.AlbumEntity import com.maxrave.simpmusic.data.db.entities.ArtistEntity import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity +import com.maxrave.simpmusic.data.db.entities.LyricsEntity import com.maxrave.simpmusic.data.db.entities.PlaylistEntity import com.maxrave.simpmusic.data.db.entities.SearchHistory import com.maxrave.simpmusic.data.db.entities.SongEntity import com.maxrave.simpmusic.data.model.browse.album.AlbumBrowse import com.maxrave.simpmusic.data.model.browse.album.Track import com.maxrave.simpmusic.data.model.browse.artist.ArtistBrowse +import com.maxrave.simpmusic.data.model.browse.artist.ChannelId import com.maxrave.simpmusic.data.model.browse.playlist.PlaylistBrowse import com.maxrave.simpmusic.data.model.explore.mood.Mood import com.maxrave.simpmusic.data.model.explore.mood.genre.GenreObject @@ -25,6 +27,7 @@ import com.maxrave.simpmusic.data.model.searchResult.artists.ArtistsResult import com.maxrave.simpmusic.data.model.searchResult.playlists.PlaylistsResult import com.maxrave.simpmusic.data.model.searchResult.songs.SongsResult import com.maxrave.simpmusic.data.model.searchResult.videos.VideosResult +import com.maxrave.simpmusic.data.model.songfull.SongFull import com.maxrave.simpmusic.data.model.streams.Streams import com.maxrave.simpmusic.data.model.thumbnailUrl import com.maxrave.simpmusic.utils.Resource @@ -40,6 +43,7 @@ import javax.inject.Inject //@ActivityRetainedScoped class MainRepository @Inject constructor(private val remoteDataSource: RemoteDataSource, private val localDataSource: LocalDataSource): BaseApiResponse() { suspend fun getSong(videoId: String): Flow>> = flow { emit(safeApiCall { remoteDataSource.getSong(videoId) }) }.flowOn(Dispatchers.IO) + suspend fun getSongFull(videoId: String): Flow> = flow { emit(safeApiCall { remoteDataSource.getSongFull(videoId) }) }.flowOn(Dispatchers.IO) suspend fun getThumbnails(songId: String): Flow>> = flow { emit(safeApiCall { remoteDataSource.getThumbnails(songId) }) }.flowOn(Dispatchers.IO) //search suspend fun searchAll(query: String, regionCode: String) = remoteDataSource.searchAll(query, regionCode) @@ -76,6 +80,7 @@ class MainRepository @Inject constructor(private val remoteDataSource: RemoteDat suspend fun getRelated(videoId: String, regionCode: String): Flow>> = flow>> { emit(safeApiCall { remoteDataSource.getRelated(videoId, regionCode) }) }.flowOn(Dispatchers.IO) suspend fun getVideoRelated(videoId: String, regionCode: String): Flow>> = flow>> { emit(safeApiCall { remoteDataSource.getVideoRelated(videoId, regionCode) }) }.flowOn(Dispatchers.IO) + suspend fun convertNameToId(name: String): Flow> = flow> { emit(safeApiCall { remoteDataSource.convertNameToId(name) }) }.flowOn(Dispatchers.IO) //Database suspend fun getSearchHistory(): Flow> = flow { emit(localDataSource.getSearchHistory()) }.flowOn(Dispatchers.IO) @@ -85,6 +90,7 @@ class MainRepository @Inject constructor(private val remoteDataSource: RemoteDat suspend fun getAllSongs(): Flow> = flow { emit(localDataSource.getAllSongs()) }.flowOn(Dispatchers.IO) suspend fun getSongsByListVideoId(listVideoId: List): Flow> = flow { emit(localDataSource.getSongByListVideoId(listVideoId)) }.flowOn(Dispatchers.IO) suspend fun getDownloadedSongs(): Flow> = flow { emit(localDataSource.getDownloadedSongs()) }.flowOn(Dispatchers.IO) + suspend fun getDownloadingSongs(): Flow> = flow { emit(localDataSource.getDownloadingSongs()) }.flowOn(Dispatchers.IO) suspend fun getLikedSongs(): Flow> = flow { emit(localDataSource.getLikedSongs()) }.flowOn(Dispatchers.IO) suspend fun getLibrarySongs(): Flow> = flow { emit(localDataSource.getLibrarySongs()) }.flowOn(Dispatchers.IO) suspend fun getSongById(id: String): Flow = flow { emit(localDataSource.getSong(id)) }.flowOn(Dispatchers.IO) @@ -132,4 +138,7 @@ class MainRepository @Inject constructor(private val remoteDataSource: RemoteDat suspend fun getAllRecentData(): Flow> = flow { emit(localDataSource.getAllRecentData()) }.flowOn(Dispatchers.IO) suspend fun getAllDownloadedPlaylist(): Flow> = flow { emit(localDataSource.getAllDownloadedPlaylist()) }.flowOn(Dispatchers.IO) suspend fun getRecentSong(limit: Int, offset: Int) = localDataSource.getRecentSongs(limit, offset) + + suspend fun getSavedLyrics(videoId: String): Flow = flow { emit(localDataSource.getSavedLyrics(videoId)) }.flowOn(Dispatchers.IO) + suspend fun insertLyrics(lyricsEntity: LyricsEntity) = withContext(Dispatchers.IO) { localDataSource.insertLyrics(lyricsEntity) } } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/di/LocalServiceModule.kt b/app/src/main/java/com/maxrave/simpmusic/di/LocalServiceModule.kt index 51bbe9ba..db1a62c4 100644 --- a/app/src/main/java/com/maxrave/simpmusic/di/LocalServiceModule.kt +++ b/app/src/main/java/com/maxrave/simpmusic/di/LocalServiceModule.kt @@ -2,6 +2,7 @@ package com.maxrave.simpmusic.di import android.content.Context import androidx.room.Room +import com.maxrave.simpmusic.common.DB_NAME import com.maxrave.simpmusic.data.db.DatabaseDao import com.maxrave.simpmusic.data.db.MusicDatabase import dagger.Module @@ -17,7 +18,7 @@ object LocalServiceModule { @Provides @Singleton - fun provideMusicDatabase(@ApplicationContext context: Context): MusicDatabase = Room.databaseBuilder(context, MusicDatabase::class.java, "Music Database").build() + fun provideMusicDatabase(@ApplicationContext context: Context): MusicDatabase = Room.databaseBuilder(context, MusicDatabase::class.java, DB_NAME).build() @Provides @Singleton diff --git a/app/src/main/java/com/maxrave/simpmusic/di/MusicServiceModule.kt b/app/src/main/java/com/maxrave/simpmusic/di/MusicServiceModule.kt index ef866d30..54129fed 100644 --- a/app/src/main/java/com/maxrave/simpmusic/di/MusicServiceModule.kt +++ b/app/src/main/java/com/maxrave/simpmusic/di/MusicServiceModule.kt @@ -19,6 +19,7 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.session.MediaLibraryService.MediaLibrarySession import androidx.media3.session.MediaSession +import com.maxrave.simpmusic.data.dataStore.DataStoreManager import com.maxrave.simpmusic.data.repository.MainRepository import com.maxrave.simpmusic.service.SimpleMediaNotificationManager import com.maxrave.simpmusic.service.SimpleMediaService @@ -167,7 +168,7 @@ object MusicServiceModule { @Provides @Singleton fun provideMusicSource( - simpleMediaServiceHandler: SimpleMediaServiceHandler, mainRepository: MainRepository + simpleMediaServiceHandler: SimpleMediaServiceHandler, mainRepository: MainRepository, dataStoreManager: DataStoreManager ): MusicSource = - MusicSource(simpleMediaServiceHandler, mainRepository) + MusicSource(simpleMediaServiceHandler, mainRepository, dataStoreManager) } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/extension/AllExt.kt b/app/src/main/java/com/maxrave/simpmusic/extension/AllExt.kt index 3e24dbaf..00486d47 100644 --- a/app/src/main/java/com/maxrave/simpmusic/extension/AllExt.kt +++ b/app/src/main/java/com/maxrave/simpmusic/extension/AllExt.kt @@ -6,7 +6,9 @@ import android.content.Context import android.view.View import android.view.ViewGroup import androidx.media3.common.MediaItem +import androidx.sqlite.db.SimpleSQLiteQuery import com.maxrave.simpmusic.data.db.entities.AlbumEntity +import com.maxrave.simpmusic.data.db.entities.LyricsEntity import com.maxrave.simpmusic.data.db.entities.PlaylistEntity import com.maxrave.simpmusic.data.db.entities.SearchHistory import com.maxrave.simpmusic.data.db.entities.SongEntity @@ -15,11 +17,18 @@ import com.maxrave.simpmusic.data.model.browse.album.Track import com.maxrave.simpmusic.data.model.browse.artist.ResultSong import com.maxrave.simpmusic.data.model.browse.playlist.PlaylistBrowse import com.maxrave.simpmusic.data.model.home.Content +import com.maxrave.simpmusic.data.model.metadata.Lyrics import com.maxrave.simpmusic.data.model.searchResult.songs.Album import com.maxrave.simpmusic.data.model.searchResult.songs.Artist import com.maxrave.simpmusic.data.model.searchResult.songs.SongsResult import com.maxrave.simpmusic.data.model.searchResult.songs.Thumbnail import com.maxrave.simpmusic.data.model.searchResult.videos.VideosResult +import com.maxrave.simpmusic.data.model.songfull.SongFull +import java.io.File +import java.io.InputStream +import java.io.OutputStream +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream fun Context.isMyServiceRunning(serviceClass: Class) = try { (getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager) @@ -240,6 +249,26 @@ fun Content.toTrack(): Track { year = "" ) } + +fun SongFull.toTrack(videoId: String): Track { + return Track( + album = null, + artists = this.artist, + duration = "", + durationSeconds = 0, + isAvailable = false, + isExplicit = false, + likeStatus = "INDIFFERENT", + thumbnails = this.thumbnails, + title = this.title, + videoId = videoId, + videoType = "Song", + category = "", + feedbackTokens = null, + resultType = null, + year = "" + ) +} fun List.toListVideoId(): List { val list = mutableListOf() for (item in this) { @@ -302,6 +331,18 @@ fun Track.addThumbnails(): Track { ) } +fun LyricsEntity.toLyrics(): Lyrics { + return Lyrics( + error = this.error, lines = this.lines, syncType = this.syncType + ) +} + +fun Lyrics.toLyricsEntity(videoId: String): LyricsEntity { + return LyricsEntity( + videoId = videoId, error = this.error, lines = this.lines, syncType = this.syncType + ) +} + fun setEnabledAll(v: View, enabled: Boolean) { v.isEnabled = enabled v.isFocusable = enabled @@ -323,4 +364,8 @@ fun ArrayList.removeConflicts(): ArrayList { return nonConflictingList } +operator fun File.div(child: String): File = File(this, child) +fun String.toSQLiteQuery(): SimpleSQLiteQuery = SimpleSQLiteQuery(this) +fun InputStream.zipInputStream(): ZipInputStream = ZipInputStream(this) +fun OutputStream.zipOutputStream(): ZipOutputStream = ZipOutputStream(this) diff --git a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaNotificationManager.kt b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaNotificationManager.kt index 4a0cd148..9a90039c 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaNotificationManager.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaNotificationManager.kt @@ -6,6 +6,8 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.PendingIntent.FLAG_IMMUTABLE +import android.app.PendingIntent.FLAG_MUTABLE +import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.Context import android.content.Intent import android.content.pm.PackageManager @@ -78,21 +80,18 @@ class SimpleMediaNotificationManager @Inject constructor( .setMediaDescriptionAdapter( SimpleMediaNotificationAdapter( context = context, -// pendingIntent = mediaSession.sessionActivity - pendingIntent = PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java).apply { - action = "show_now_playing" - }, FLAG_IMMUTABLE) + pendingIntent = PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), FLAG_IMMUTABLE) ) ) .setNotificationListener(notificationListener) - .setSmallIconResourceId(R.drawable.ic_microphone) + .setSmallIconResourceId(R.drawable.logo_simpmusic_01_removebg_preview) .build() .also { it.setMediaSessionToken(mediaSession.sessionCompatToken) it.setUseFastForwardActionInCompactView(true) it.setUseRewindActionInCompactView(true) it.setUseNextActionInCompactView(false) - it.setPriority(NotificationCompat.PRIORITY_HIGH) + it.setPriority(NotificationCompat.PRIORITY_LOW) it.setPlayer(player) } } diff --git a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt index d5e6208f..0f6558ab 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt @@ -5,9 +5,11 @@ import android.app.Application import android.content.Intent import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.cache.SimpleCache import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService +import com.maxrave.simpmusic.di.PlayerCache import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -39,6 +41,7 @@ class SimpleMediaService : MediaSessionService() { return START_STICKY_COMPATIBILITY } + @UnstableApi override fun onDestroy() { super.onDestroy() mediaSession.run { @@ -60,6 +63,4 @@ class SimpleMediaService : MediaSessionService() { } } -const val NEXT = "next" -const val PREVIOUS = "previous" diff --git a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt index bce2b877..5c4732de 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt @@ -60,6 +60,10 @@ class SimpleMediaServiceHandler @Inject constructor( return player.getMediaItemAt(index) } + fun removeMediaItem(position: Int) { + player.removeMediaItem(position) + } + fun addMediaItem(mediaItem: MediaItem) { player.clearMediaItems() player.setMediaItem(mediaItem) diff --git a/app/src/main/java/com/maxrave/simpmusic/service/test/download/MusicDownloadService.kt b/app/src/main/java/com/maxrave/simpmusic/service/test/download/MusicDownloadService.kt index 25e97c03..6bb40049 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/test/download/MusicDownloadService.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/test/download/MusicDownloadService.kt @@ -37,7 +37,7 @@ class MusicDownloadService : DownloadService( override fun getForegroundNotification(downloads: MutableList, notMetRequirements: Int): Notification = downloadUtil.downloadNotificationHelper.buildProgressNotification( this, - R.drawable.baseline_downloaded, + R.drawable.logo_simpmusic_01_removebg_preview, null, if (downloads.size == 1) Util.fromUtf8Bytes(downloads[0].request.data) else resources.getQuantityString(R.plurals.n_song, downloads.size, downloads.size), diff --git a/app/src/main/java/com/maxrave/simpmusic/service/test/source/MusicSource.kt b/app/src/main/java/com/maxrave/simpmusic/service/test/source/MusicSource.kt index ca4bbb45..dced97fd 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/test/source/MusicSource.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/test/source/MusicSource.kt @@ -5,6 +5,8 @@ import androidx.core.net.toUri import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.util.UnstableApi +import com.maxrave.simpmusic.common.QUALITY +import com.maxrave.simpmusic.data.dataStore.DataStoreManager import com.maxrave.simpmusic.data.model.browse.album.Track import com.maxrave.simpmusic.data.queue.Queue import com.maxrave.simpmusic.data.repository.MainRepository @@ -18,9 +20,11 @@ import com.maxrave.simpmusic.service.test.source.StateSource.STATE_INITIALIZING import com.maxrave.simpmusic.utils.Resource import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import javax.inject.Inject -class MusicSource @Inject constructor(val simpleMediaServiceHandler: SimpleMediaServiceHandler, private val mainRepository: MainRepository) { +class MusicSource @Inject constructor(val simpleMediaServiceHandler: SimpleMediaServiceHandler, private val mainRepository: MainRepository, private val dataStoreManager: DataStoreManager) { var catalogMetadata: ArrayList = (arrayListOf()) var downloadUrl: ArrayList = arrayListOf() @@ -83,6 +87,12 @@ class MusicSource @Inject constructor(val simpleMediaServiceHandler: SimpleMedia private suspend fun updateCatalog(downloaded: Int = 0): Boolean { state = STATE_INITIALIZING val tempQueue = Queue.getQueue() + val quality = runBlocking { dataStoreManager.quality.first() } + val itag = when (quality) { + QUALITY.items[0].toString() -> 250 + QUALITY.items[1].toString() -> 251 + else -> 251 + } for (i in 0 until tempQueue.size){ val track = tempQueue[i] var thumbUrl = track.thumbnails?.last()?.url ?: "http://i.ytimg.com/vi/${track.videoId}/maxresdefault.jpg" @@ -113,32 +123,34 @@ class MusicSource @Inject constructor(val simpleMediaServiceHandler: SimpleMedia Log.d("MusicSource", "updateCatalog: $values") when (values) { is Resource.Success -> { - val listAudioStream = values.data - listAudioStream?.forEach { - if (it.itag == 251) { - val uri = it.url - val artistName: String = - track.artists.toListName().connectArtists() - Log.d("Check URI", uri) - simpleMediaServiceHandler.addMediaItemNotSet(MediaItem.Builder().setUri(uri) - .setMediaId(track.videoId) - .setMediaMetadata( - MediaMetadata.Builder() - .setTitle(track.title) - .setArtist(artistName) - .setArtworkUri(thumbUrl.toUri()) - .setAlbumTitle(track.album?.name) - .build() + if (!catalogMetadata.contains(track)) { + val listAudioStream = values.data + listAudioStream?.forEach { + if (it.itag == itag) { + val uri = it.url + val artistName: String = + track.artists.toListName().connectArtists() + Log.d("Check URI", uri) + simpleMediaServiceHandler.addMediaItemNotSet(MediaItem.Builder().setUri(uri) + .setMediaId(track.videoId) + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(track.title) + .setArtist(artistName) + .setArtworkUri(thumbUrl.toUri()) + .setAlbumTitle(track.album?.name) + .build() + ) + .build()) + catalogMetadata.add(track) + Log.d( + "MusicSource", + "updateCatalog: ${track.title}, ${catalogMetadata.size}" ) - .build()) - catalogMetadata.add(track) - Log.d( - "MusicSource", - "updateCatalog: ${track.title}, ${catalogMetadata.size}" - ) - downloadUrl.add(uri) - added.value = true - Log.d("MusicSource", "updateCatalog: ${track.title}") + downloadUrl.add(uri) + added.value = true + Log.d("MusicSource", "updateCatalog: ${track.title}") + } } } } @@ -160,6 +172,31 @@ class MusicSource @Inject constructor(val simpleMediaServiceHandler: SimpleMedia catalogMetadata.add(0, it) Log.d("MusicSource", "addFirstMetadata: ${it.title}, ${catalogMetadata.size}") } + + @UnstableApi + fun moveItemUp(position: Int) { + simpleMediaServiceHandler.moveMediaItem(position, position - 1) + val temp = catalogMetadata[position] + catalogMetadata[position] = catalogMetadata[position - 1] + catalogMetadata[position - 1] = temp + _currentSongIndex.value = simpleMediaServiceHandler.currentIndex() + } + + @UnstableApi + fun moveItemDown(position: Int) { + simpleMediaServiceHandler.moveMediaItem(position, position + 1) + val temp = catalogMetadata[position] + catalogMetadata[position] = catalogMetadata[position + 1] + catalogMetadata[position + 1] = temp + _currentSongIndex.value = simpleMediaServiceHandler.currentIndex() + } + + @UnstableApi + fun removeMediaItem(position: Int) { + simpleMediaServiceHandler.removeMediaItem(position) + catalogMetadata.removeAt(position) + _currentSongIndex.value = simpleMediaServiceHandler.currentIndex() + } } enum class StateSource { diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt b/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt index f525964d..292c01d7 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt @@ -24,12 +24,18 @@ import coil.request.ImageRequest import coil.size.Size import coil.transform.Transformation import android.Manifest +import android.net.Uri +import android.widget.Toast +import androidx.core.net.toUri +import androidx.media3.exoplayer.offline.DownloadService +import com.daimajia.swipe.SwipeLayout import com.maxrave.simpmusic.R import com.maxrave.simpmusic.common.Config import com.maxrave.simpmusic.data.queue.Queue import com.maxrave.simpmusic.databinding.ActivityMainBinding import com.maxrave.simpmusic.extension.connectArtists import com.maxrave.simpmusic.extension.isMyServiceRunning +import com.maxrave.simpmusic.extension.toTrack import com.maxrave.simpmusic.service.SimpleMediaService import com.maxrave.simpmusic.service.test.source.FetchQueue import com.maxrave.simpmusic.ui.fragment.player.NowPlayingFragment @@ -37,6 +43,7 @@ import com.maxrave.simpmusic.utils.Resource import com.maxrave.simpmusic.viewModel.SharedViewModel import com.maxrave.simpmusic.viewModel.UIEvent import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import pub.devrel.easypermissions.EasyPermissions @@ -45,12 +52,22 @@ import pub.devrel.easypermissions.EasyPermissions class MainActivity : AppCompatActivity(), NowPlayingFragment.OnNowPlayingSongChangeListener { private lateinit var binding: ActivityMainBinding private val viewModel by viewModels() + private var action: String? = null + private var data: Uri? = null override fun onResume() { super.onResume() viewModel.getCurrentMediaItem() } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + action = intent?.action + data = intent?.data ?: intent?.getStringExtra(Intent.EXTRA_TEXT)?.toUri() + Log.d("MainActivity", "onNewIntent: $data") + viewModel.intent.value = intent + } + override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, @@ -63,7 +80,8 @@ class MainActivity : AppCompatActivity(), NowPlayingFragment.OnNowPlayingSongCha @UnstableApi override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + action = intent.action + viewModel.checkIsRestoring() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { WindowCompat.setDecorFitsSystemWindows(window, false) @@ -81,10 +99,61 @@ class MainActivity : AppCompatActivity(), NowPlayingFragment.OnNowPlayingSongCha binding = ActivityMainBinding.inflate(layoutInflater) val view = binding.root setContentView(view) - binding.card.visibility = View.GONE + binding.miniplayer.visibility = View.GONE val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment_container_view) val navController = navHostFragment?.findNavController() binding.bottomNavigationView.setupWithNavController(navController!!) + when (action) { + "com.maxrave.simpmusic.action.HOME" -> { + binding.bottomNavigationView.selectedItemId = R.id.bottom_navigation_item_home + } + "com.maxrave.simpmusic.action.SEARCH" -> { + binding.bottomNavigationView.selectedItemId = R.id.bottom_navigation_item_search + } + "com.maxrave.simpmusic.action.LIBRARY" -> { + binding.bottomNavigationView.selectedItemId = R.id.bottom_navigation_item_library + } + else -> {} + } + + binding.miniplayer.showMode = SwipeLayout.ShowMode.PullOut + binding.miniplayer.addDrag(SwipeLayout.DragEdge.Right, binding.llBottom) + binding.miniplayer.addSwipeListener(object : SwipeLayout.SwipeListener { + override fun onStartOpen(layout: SwipeLayout?) { + binding.card.radius = 0f + } + + override fun onOpen(layout: SwipeLayout?) { + binding.card.radius = 0f + } + + override fun onStartClose(layout: SwipeLayout?) { + } + + override fun onClose(layout: SwipeLayout?) { + binding.card.radius = 8f + } + + override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) { + } + + override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) { + } + + }) + binding.btRemoveMiniPlayer.setOnClickListener { + if (viewModel.isServiceRunning.value == true){ + stopService(Intent(this, SimpleMediaService::class.java)) + Log.d("Service", "Service stopped") + if (this.isMyServiceRunning(FetchQueue:: class.java)){ + stopService(Intent(this, FetchQueue::class.java)) + Log.d("Service", "FetchQueue stopped") + } + viewModel.isServiceRunning.postValue(false) + } + viewModel.videoId.postValue(null) + binding.miniplayer.visibility = View.GONE + } binding.card.setOnClickListener { val bundle = Bundle() @@ -95,18 +164,85 @@ class MainActivity : AppCompatActivity(), NowPlayingFragment.OnNowPlayingSongCha viewModel.onUIEvent(UIEvent.PlayPause) } lifecycleScope.launch { -// val job3 = launch { -// viewModel.videoId.observe(this@MainActivity){ -// if (it != null){ -// if (viewModel.songTransitions.value){ -// Log.i("Now Playing Fragment", "Ở Activity") -// Log.d("Song Transition", "Song Transition") -// viewModel.getMetadata(it) -// viewModel.changeSongTransitionToFalse() -// } -// } -// } -// } + val job1 = launch { + viewModel.intent.collectLatest { intent -> + if (intent != null){ + data = intent.data ?: intent.getStringExtra(Intent.EXTRA_TEXT)?.toUri() + Log.d("MainActivity", "onCreate: $data") + if (data != null) { + when (val path = data!!.pathSegments.firstOrNull()) { + "playlist" -> data!!.getQueryParameter("list")?.let { playlistId -> + if (playlistId.startsWith("OLAK5uy_")) { + viewModel.intent.value = null + navController.navigate(R.id.action_global_albumFragment, Bundle().apply { + putString("browseId", playlistId) + }) + } else { + viewModel.intent.value = null + navController.navigate(R.id.action_global_playlistFragment, Bundle().apply { + putString("id", playlistId) + }) + } + } + + "channel", "c" -> data!!.lastPathSegment?.let { artistId -> + if (artistId.startsWith("UC")) { + viewModel.intent.value = null + navController.navigate(R.id.action_global_artistFragment, Bundle().apply { + putString("channelId", artistId) + }) + } + else { + viewModel.convertNameToId(artistId) + viewModel.artistId.observe(this@MainActivity) {channelId -> + when (channelId) { + is Resource.Success -> { + viewModel.intent.value = null + navController.navigate(R.id.action_global_artistFragment, Bundle().apply { + putString("channelId", channelId.data?.id) + }) + } + is Resource.Error -> { + viewModel.intent.value = null + Toast.makeText(this@MainActivity, channelId.message, Toast.LENGTH_SHORT).show() + } + } + } + } + } + + else -> when { + path == "watch" -> data!!.getQueryParameter("v") + data!!.host == "youtu.be" -> path + else -> null + }?.let { videoId -> + viewModel.getSongFull(videoId) + viewModel.songFull.observe(this@MainActivity) { + when (it) { + is Resource.Success -> { + val song = it.data!! + val track = song.toTrack(videoId) + Queue.clear() + Queue.setNowPlaying(track) + val args = Bundle() + args.putString("videoId", videoId) + args.putString("from", "Shared") + args.putString("type", Config.SONG_CLICK) + viewModel.intent.value = null + navController.navigate(R.id.action_global_nowPlayingFragment, args) + } + is Resource.Error -> { + viewModel.intent.value = null + Toast.makeText(this@MainActivity, it.message, Toast.LENGTH_SHORT).show() + } + } + } + } + } + } + } + } + } val job5 = launch { viewModel.nowPlayingMediaItem.observe(this@MainActivity){ if (it != null){ @@ -150,7 +286,6 @@ class MainActivity : AppCompatActivity(), NowPlayingFragment.OnNowPlayingSongCha } Log.d("Check Start Color", "transform: $startColor") } -// val centerColor = 0x6C6C6C val endColor = 0x1b1a1f val gd = GradientDrawable( GradientDrawable.Orientation.TOP_BOTTOM, @@ -162,6 +297,7 @@ class MainActivity : AppCompatActivity(), NowPlayingFragment.OnNowPlayingSongCha gd.alpha = 150 val bg = ColorUtils.setAlphaComponent(startColor, 230) binding.card.setCardBackgroundColor(bg) + binding.cardBottom.setCardBackgroundColor(bg) return input } @@ -171,16 +307,6 @@ class MainActivity : AppCompatActivity(), NowPlayingFragment.OnNowPlayingSongCha } } } -// val job1 = launch { -// viewModel.metadata.observe(this@MainActivity){ -// if (it is Resource.Success){ -// if (viewModel.isServiceRunning.value == false){ -// startService() -// viewModel.isServiceRunning.postValue(true) -// } -// } -// } -// } val job2 = launch { viewModel.progress.collect{ binding.progressBar.progress = (it * 100).toInt() @@ -192,7 +318,7 @@ class MainActivity : AppCompatActivity(), NowPlayingFragment.OnNowPlayingSongCha binding.cbFavorite.isChecked = liked } } - //job1.join() + job1.join() job2.join() //job3.join() job5.join() @@ -228,18 +354,23 @@ class MainActivity : AppCompatActivity(), NowPlayingFragment.OnNowPlayingSongCha } private fun stopService(){ if (viewModel.isServiceRunning.value == true){ - val intent = Intent(this, SimpleMediaService::class.java) - stopService(intent) - viewModel.isServiceRunning.postValue(false) + stopService(Intent(this, SimpleMediaService::class.java)) Log.d("Service", "Service stopped") if (this.isMyServiceRunning(FetchQueue:: class.java)){ - this.stopService(Intent(this, FetchQueue::class.java)) + stopService(Intent(this, FetchQueue::class.java)) + Log.d("Service", "FetchQueue stopped") + } + if (this.isMyServiceRunning(DownloadService:: class.java)){ + this.stopService(Intent(this, DownloadService::class.java)) + viewModel.changeAllDownloadingToError() + Log.d("Service", "DownloadService stopped") } + viewModel.isServiceRunning.postValue(false) } } override fun onNowPlayingSongChange() { - viewModel.metadata.observe(this, Observer { + viewModel.metadata.observe(this) { when(it){ is Resource.Success -> { binding.songTitle.text = it.data?.title @@ -260,7 +391,7 @@ class MainActivity : AppCompatActivity(), NowPlayingFragment.OnNowPlayingSongCha } } - }) + } } override fun onIsPlayingChange() { diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/SearchFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/SearchFragment.kt index 9afdfcb4..f897bb11 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/SearchFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/SearchFragment.kt @@ -20,6 +20,7 @@ import androidx.media3.exoplayer.offline.DownloadService import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import coil.load +import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.snackbar.Snackbar import com.maxrave.simpmusic.R @@ -690,11 +691,12 @@ class SearchFragment : Fragment() { } is Resource.Error -> { response.message?.let { message -> - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchSearchVideos(query) } - .setDuration(5000) + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) + .setDuration(3000) .show() } binding.shimmerLayout.stopShimmer() @@ -732,11 +734,12 @@ class SearchFragment : Fragment() { } is Resource.Error -> { response.message?.let { message -> - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchSearchAlbums(query) } - .setDuration(5000) + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) + .setDuration(3000) .show() } binding.shimmerLayout.stopShimmer() @@ -775,11 +778,12 @@ class SearchFragment : Fragment() { } is Resource.Error -> { response.message?.let { message -> - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchSearchPlaylists(query) } - .setDuration(5000) + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) + .setDuration(3000) .show() } binding.shimmerLayout.stopShimmer() @@ -819,11 +823,12 @@ class SearchFragment : Fragment() { } is Resource.Error -> { response.message?.let { message -> - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchSearchArtists(query) } - .setDuration(5000) + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) + .setDuration(3000) .show() } binding.shimmerLayout.stopShimmer() @@ -863,10 +868,11 @@ class SearchFragment : Fragment() { } is Resource.Error -> { response.message?.let { message -> - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchSearchSongs(query) } + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) .setDuration(5000) .show() } @@ -905,11 +911,12 @@ class SearchFragment : Fragment() { is Resource.Error -> { response.message?.let { message -> - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchSearchAll(query) } - .setDuration(5000) + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) + .setDuration(3000) .show() } } @@ -927,11 +934,12 @@ class SearchFragment : Fragment() { } is Resource.Error -> { response.message?.let { message -> - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchSearchAll(query) } - .setDuration(5000) + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) + .setDuration(3000) .show() } } @@ -947,11 +955,12 @@ class SearchFragment : Fragment() { } is Resource.Error -> { response.message?.let { message -> - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchSearchAll(query) } - .setDuration(5000) + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) + .setDuration(3000) .show() } } @@ -967,11 +976,12 @@ class SearchFragment : Fragment() { } is Resource.Error -> { response.message?.let { message -> - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchSearchAll(query) } - .setDuration(5000) + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) + .setDuration(3000) .show() } } @@ -987,11 +997,12 @@ class SearchFragment : Fragment() { } is Resource.Error -> { response.message?.let { message -> - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchSearchAll(query) } - .setDuration(5000) + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) + .setDuration(3000) .show() } } @@ -1033,11 +1044,12 @@ class SearchFragment : Fragment() { } } catch (e: Exception){ - Snackbar.make(binding.root, e.message.toString(), Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), e.message.toString(), Snackbar.LENGTH_LONG) .setAction("Retry") { fetchSearchAll(query) } - .setDuration(5000) + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) + .setDuration(3000) .show() } // val sortedList = temp.sortedWith(compareByDescending diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/home/HomeFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/home/HomeFragment.kt index 02f76d51..bb4f73ea 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/home/HomeFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/home/HomeFragment.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import com.maxrave.simpmusic.R import com.maxrave.simpmusic.adapter.home.GenreAdapter @@ -402,10 +403,11 @@ class HomeFragment : Fragment() { binding.fullLayout.visibility = View.VISIBLE binding.swipeRefreshLayout.isRefreshing = false response.message?.let { message -> - Snackbar.make(binding.root, "Home Data Error "+message, Snackbar.LENGTH_LONG) + Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchHomeData() } + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) .setDuration(3000) .show() } @@ -430,10 +432,11 @@ class HomeFragment : Fragment() { is Resource.Error -> { binding.swipeRefreshLayout.isRefreshing = false response.message?.let { message -> - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchHomeData() } + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) .setDuration(5000) .show() } @@ -462,10 +465,11 @@ class HomeFragment : Fragment() { } is Resource.Error -> { response.message?.let { message -> - Snackbar.make(binding.root, "Chart Load Error "+ message, Snackbar.LENGTH_LONG) + Snackbar.make(requireActivity().findViewById(R.id.mini_player_container), message, Snackbar.LENGTH_LONG) .setAction("Retry") { fetchHomeData() } + .setAnchorView(activity?.findViewById(R.id.bottom_navigation_view)) .setDuration(5000) .show() } diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/home/SettingsFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/home/SettingsFragment.kt index 781e75c1..2992bcc2 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/home/SettingsFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/home/SettingsFragment.kt @@ -8,18 +8,26 @@ import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.viewModels import androidx.media3.common.util.UnstableApi import androidx.navigation.fragment.findNavController +import coil.Coil +import coil.annotation.ExperimentalCoilApi +import coil.imageLoader import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.maxrave.simpmusic.R import com.maxrave.simpmusic.common.Config +import com.maxrave.simpmusic.common.QUALITY import com.maxrave.simpmusic.common.SUPPORTED_LOCATION import com.maxrave.simpmusic.databinding.FragmentSettingsBinding import com.maxrave.simpmusic.viewModel.SettingsViewModel import dagger.hilt.android.AndroidEntryPoint import dev.chrisbanes.insetter.applyInsetter +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +@UnstableApi @AndroidEntryPoint class SettingsFragment : Fragment() { @@ -27,6 +35,17 @@ class SettingsFragment : Fragment() { private val binding get() = _binding!! private val viewModel by viewModels() + val backupLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("application/octet-stream")) { uri -> + if (uri != null) { + viewModel.backup(requireContext(), uri) + } + } + val restoreLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + if (uri != null) { + viewModel.restore(requireContext(), uri) + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, @@ -41,22 +60,34 @@ class SettingsFragment : Fragment() { } + @OptIn(ExperimentalCoilApi::class) @UnstableApi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.getLocation() + viewModel.getQuality() viewModel.getPlayerCacheSize() viewModel.getDownloadedCacheSize() + val diskCache = context?.imageLoader?.diskCache + viewModel.location.observe(viewLifecycleOwner) { binding.tvContentCountry.text = it } + viewModel.quality.observe(viewLifecycleOwner) { + binding.tvQuality.text = it + } viewModel.cacheSize.observe(viewLifecycleOwner) { binding.tvPlayerCache.text = getString(R.string.cache_size, bytesToMB(it).toString()) } viewModel.downloadedCacheSize.observe(viewLifecycleOwner) { binding.tvDownloadedCache.text = getString(R.string.cache_size, bytesToMB(it).toString()) } + binding.tvThumbnailCache.text = getString(R.string.cache_size, if (diskCache?.size != null) { + bytesToMB(diskCache.size) + } else { + 0 + }.toString()) binding.btVersion.setOnClickListener { val urlIntent = Intent( @@ -116,6 +147,28 @@ class SettingsFragment : Fragment() { } dialog.show() } + binding.btQuality.setOnClickListener { + var checkedIndex = -1 + val dialog = MaterialAlertDialogBuilder(requireContext()) + .setSingleChoiceItems(QUALITY.items, -1) { _, which -> + checkedIndex = which + } + .setTitle("Quality") + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + .setPositiveButton("Change") { dialog, _ -> + if (checkedIndex != -1) { + viewModel.changeQuality(checkedIndex) + viewModel.quality.observe(viewLifecycleOwner) { + binding.tvQuality.text = it + } + } + dialog.dismiss() + } + dialog.show() + + } binding.btStorageDownloadedCache.setOnClickListener { val dialog = MaterialAlertDialogBuilder(requireContext()) @@ -133,10 +186,37 @@ class SettingsFragment : Fragment() { dialog.show() } + binding.btStorageThumbnailCache.setOnClickListener { + val dialog = MaterialAlertDialogBuilder(requireContext()) + .setTitle("Clear Thumbnail Cache") + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + .setPositiveButton("Clear") { dialog, _ -> + diskCache?.clear() + binding.tvThumbnailCache.text = getString(R.string.cache_size, if (diskCache?.size != null) { + bytesToMB(diskCache.size) + } else { + 0 + }.toString()) + dialog.dismiss() + } + dialog.show() + } + binding.topAppBar.setNavigationOnClickListener { findNavController().popBackStack() } + + binding.btBackup.setOnClickListener { + val formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss") + backupLauncher.launch("${getString(R.string.app_name)}_${LocalDateTime.now().format(formatter)}.backup") + } + + binding.btRestore.setOnClickListener { + restoreLauncher.launch(arrayOf("application/octet-stream")) + } } private fun bytesToMB(bytes: Long): Long { diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/library/DownloadedFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/library/DownloadedFragment.kt index 88b1aee3..e363c41d 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/library/DownloadedFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/library/DownloadedFragment.kt @@ -17,17 +17,21 @@ import coil.load import com.google.android.material.bottomsheet.BottomSheetDialog import com.maxrave.simpmusic.R import com.maxrave.simpmusic.adapter.artist.SeeArtistOfNowPlayingAdapter +import com.maxrave.simpmusic.adapter.playlist.AddToAPlaylistAdapter import com.maxrave.simpmusic.adapter.search.SearchItemAdapter import com.maxrave.simpmusic.common.Config import com.maxrave.simpmusic.common.DownloadState +import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity import com.maxrave.simpmusic.data.db.entities.SongEntity import com.maxrave.simpmusic.data.model.browse.album.Track import com.maxrave.simpmusic.data.model.searchResult.songs.Artist import com.maxrave.simpmusic.data.queue.Queue +import com.maxrave.simpmusic.databinding.BottomSheetAddToAPlaylistBinding import com.maxrave.simpmusic.databinding.BottomSheetNowPlayingBinding import com.maxrave.simpmusic.databinding.BottomSheetSeeArtistOfNowPlayingBinding import com.maxrave.simpmusic.databinding.FragmentDownloadedBinding import com.maxrave.simpmusic.extension.connectArtists +import com.maxrave.simpmusic.extension.removeConflicts import com.maxrave.simpmusic.extension.setEnabledAll import com.maxrave.simpmusic.extension.toTrack import com.maxrave.simpmusic.service.test.download.MusicDownloadService @@ -172,6 +176,40 @@ class DownloadedFragment : Fragment() { } } } + btAddPlaylist.setOnClickListener { + viewModel.getAllLocalPlaylist() + val listLocalPlaylist: ArrayList = arrayListOf() + val addPlaylistDialog = BottomSheetDialog(requireContext()) + val viewAddPlaylist = BottomSheetAddToAPlaylistBinding.inflate(layoutInflater) + val addToAPlaylistAdapter = AddToAPlaylistAdapter(arrayListOf()) + viewAddPlaylist.rvLocalPlaylists.apply { + adapter = addToAPlaylistAdapter + layoutManager = LinearLayoutManager(requireContext()) + } + viewModel.localPlaylist.observe(viewLifecycleOwner) {list -> + Log.d("Check Local Playlist", list.toString()) + listLocalPlaylist.clear() + listLocalPlaylist.addAll(list) + addToAPlaylistAdapter.updateList(listLocalPlaylist) + } + addToAPlaylistAdapter.setOnItemClickListener(object : AddToAPlaylistAdapter.OnItemClickListener{ + override fun onItemClick(position: Int) { + val playlist = listLocalPlaylist[position] + val tempTrack = ArrayList() + if (playlist.tracks != null) { + tempTrack.addAll(playlist.tracks) + } + tempTrack.add(song.videoId) + tempTrack.removeConflicts() + viewModel.updateLocalPlaylistTracks(tempTrack, playlist.id) + addPlaylistDialog.dismiss() + dialog.dismiss() + } + }) + addPlaylistDialog.setContentView(viewAddPlaylist.root) + addPlaylistDialog.setCancelable(true) + addPlaylistDialog.show() + } btDownload.setOnClickListener { if (tvDownload.text == getString(R.string.downloaded)){ DownloadService.sendRemoveDownload( diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/library/LibraryFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/library/LibraryFragment.kt index 9c0b9254..5bf3992a 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/library/LibraryFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/library/LibraryFragment.kt @@ -414,7 +414,6 @@ class LibraryFragment : Fragment() { val title = viewDialog.etPlaylistName.editText?.text.toString() if (title.isNotEmpty()){ viewModel.createPlaylist(title) - viewModel.getLocalPlaylist() viewModel.listLocalPlaylist.observe(viewLifecycleOwner) { list -> val temp: ArrayList = arrayListOf() for (i in list.size - 1 downTo 0) { diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/other/LocalPlaylistFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/other/LocalPlaylistFragment.kt index 880fc940..426b770b 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/other/LocalPlaylistFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/other/LocalPlaylistFragment.kt @@ -108,7 +108,6 @@ class LocalPlaylistFragment : Fragment() { if (id == null) { id = viewModel.id.value -// fetchDataFromViewModel() fetchDataFromDatabase() binding.loadingLayout.visibility = View.GONE binding.rootLayout.visibility = View.VISIBLE @@ -299,7 +298,8 @@ class LocalPlaylistFragment : Fragment() { if (localPlaylist != null) { binding.topAppBar.title = localPlaylist.title } - binding.tvTrackCountAndTimeCreated.text = getString(R.string.album_length, localPlaylist?.tracks?.size.toString(), localPlaylist?.inLibrary?.format( + binding.tvTrackCountAndTimeCreated.text = getString(R.string.album_length, + localPlaylist?.tracks?.size?.toString() ?: "0", localPlaylist?.inLibrary?.format( DateTimeFormatter.ofPattern("HH:mm:ss dd/MM/yyyy") )) loadImage(localPlaylist?.thumbnail) diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/NowPlayingFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/NowPlayingFragment.kt index e9cc042e..871f9889 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/NowPlayingFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/NowPlayingFragment.kt @@ -34,9 +34,9 @@ import coil.request.CachePolicy import coil.request.ImageRequest import coil.size.Size import coil.transform.Transformation +import com.daimajia.swipe.SwipeLayout import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.card.MaterialCardView import com.google.android.material.slider.Slider import com.maxrave.simpmusic.R import com.maxrave.simpmusic.adapter.artist.SeeArtistOfNowPlayingAdapter @@ -44,6 +44,7 @@ import com.maxrave.simpmusic.adapter.playlist.AddToAPlaylistAdapter import com.maxrave.simpmusic.common.Config import com.maxrave.simpmusic.common.DownloadState import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity +import com.maxrave.simpmusic.data.model.browse.album.Track import com.maxrave.simpmusic.data.model.metadata.MetadataSong import com.maxrave.simpmusic.data.queue.Queue import com.maxrave.simpmusic.databinding.BottomSheetAddToAPlaylistBinding @@ -130,9 +131,9 @@ class NowPlayingFragment : Fragment() { super.onViewCreated(view, savedInstanceState) val activity = requireActivity() val bottom = activity.findViewById(R.id.bottom_navigation_view) - val card = activity.findViewById(R.id.card) + val miniplayer = activity.findViewById(R.id.miniplayer) bottom.visibility = View.GONE - card.visibility = View.GONE + miniplayer.visibility = View.GONE binding.lyricsFullLayout.visibility = View.GONE binding.buffered.max = 100 Log.d("check Video ID in ViewModel", viewModel.videoId.value.toString()) @@ -176,9 +177,9 @@ class NowPlayingFragment : Fragment() { viewModel.from.postValue(from) viewModel.resetLyrics() if (it.artists.isNullOrEmpty()) { - viewModel.getLyrics(it.title) + viewModel.getLyrics(it.title, it.videoId) } else { - viewModel.getLyrics(it.title + " " + it.artists.first().name) + viewModel.getLyrics(it.title + " " + it.artists.first().name, it.videoId) } updateUIfromQueueNowPlaying() lifecycleScope.launch { @@ -225,7 +226,7 @@ class NowPlayingFragment : Fragment() { viewModel.videoId.postValue(it.videoId) viewModel.from.postValue(from) viewModel.resetLyrics() - viewModel.getLyrics(it.title + " " + it.artists?.first()?.name) + viewModel.getLyrics(it.title + " " + it.artists?.first()?.name, it.videoId) updateUIfromQueueNowPlaying() lifecycleScope.launch { viewModel.firstTrackAdded.collect { added -> @@ -270,7 +271,7 @@ class NowPlayingFragment : Fragment() { viewModel.videoId.postValue(it.videoId) viewModel.from.postValue(from) viewModel.resetLyrics() - viewModel.getLyrics(it.title + " " + it.artists?.first()?.name) + viewModel.getLyrics(it.title + " " + it.artists?.first()?.name, it.videoId) updateUIfromQueueNowPlaying() Log.d("check index", index.toString()) lifecycleScope.launch { @@ -317,7 +318,7 @@ class NowPlayingFragment : Fragment() { viewModel.videoId.postValue(it.videoId) viewModel.from.postValue(from) viewModel.resetLyrics() - viewModel.getLyrics(it.title + " " + it.artists?.first()?.name) + viewModel.getLyrics(it.title + " " + it.artists?.first()?.name, it.videoId) updateUIfromQueueNowPlaying() Log.d("check index", index.toString()) lifecycleScope.launch { @@ -517,24 +518,6 @@ class NowPlayingFragment : Fragment() { binding.cbFavorite.isChecked = liked } } -// val job11 = launch { -// viewModel.nextTrackAvailable.collect { nextTrackAvailable -> -// if (nextTrackAvailable) { -// setEnabledAll(binding.btNext, true) -// } else { -// setEnabledAll(binding.btNext, false) -// } -// } -// } -// val job12 = launch { -// viewModel.previousTrackAvailable.collect { previousTrackAvailable -> -// if (previousTrackAvailable) { -// setEnabledAll(binding.btPrevious, true) -// } else { -// setEnabledAll(binding.btPrevious, false) -// } -// } -// } job1.join() job2.join() @@ -886,7 +869,16 @@ class NowPlayingFragment : Fragment() { is Resource.Success -> { val data = response.data!! val queue = data.toListTrack() - Queue.addAll(queue) + queue.add(Queue.getNowPlaying()!!) + val listWithoutDuplicateElements: ArrayList = ArrayList() + for (element in queue) { + // Check if element not exist in list, perform add element to list + if (!listWithoutDuplicateElements.contains(element)) { + listWithoutDuplicateElements.add(element) + } + } + Log.d("Queue", "getVideosRelated: ${listWithoutDuplicateElements.size}") + Queue.addAll(listWithoutDuplicateElements) if (!requireContext().isMyServiceRunning(FetchQueue::class.java)) { requireActivity().startService( Intent( @@ -923,7 +915,17 @@ class NowPlayingFragment : Fragment() { viewModel.related.observe(viewLifecycleOwner) { response -> when (response) { is Resource.Success -> { - Queue.addAll(response.data!!) + val data = response.data!! + data.add(Queue.getNowPlaying()!!) + val listWithoutDuplicateElements: ArrayList = ArrayList() + for (element in data) { + // Check if element not exist in list, perform add element to list + if (!listWithoutDuplicateElements.contains(element)) { + listWithoutDuplicateElements.add(element) + } + } + Log.d("Queue", "getRelated: ${listWithoutDuplicateElements.size}") + Queue.addAll(listWithoutDuplicateElements) if (!requireContext().isMyServiceRunning(FetchQueue::class.java)) { requireActivity().startService( Intent( @@ -1189,12 +1191,11 @@ class NowPlayingFragment : Fragment() { super.onDestroyView() val activity = requireActivity() val bottom = activity.findViewById(R.id.bottom_navigation_view) - val card = activity.findViewById(R.id.card) + val miniplayer = activity.findViewById(R.id.miniplayer) bottom.animation = AnimationUtils.loadAnimation(requireContext(), R.anim.bottom_to_top) bottom.visibility = View.VISIBLE - card.animation = AnimationUtils.loadAnimation(requireContext(), R.anim.bottom_to_top) - card.visibility = View.VISIBLE + miniplayer.animation = AnimationUtils.loadAnimation(requireContext(), R.anim.bottom_to_top) + miniplayer.visibility = View.VISIBLE } - } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/QueueFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/QueueFragment.kt index e4a8d557..ce6e545d 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/QueueFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/QueueFragment.kt @@ -17,7 +17,9 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.maxrave.simpmusic.R import com.maxrave.simpmusic.adapter.queue.QueueAdapter +import com.maxrave.simpmusic.databinding.BottomSheetQueueTrackOptionBinding import com.maxrave.simpmusic.databinding.QueueBottomSheetBinding +import com.maxrave.simpmusic.extension.setEnabledAll import com.maxrave.simpmusic.service.test.source.MusicSource import com.maxrave.simpmusic.service.test.source.StateSource import com.maxrave.simpmusic.viewModel.SharedViewModel @@ -157,6 +159,47 @@ class QueueFragment: BottomSheetDialogFragment() { dismiss() } }) + queueAdapter.setOnOptionClickListener(object : QueueAdapter.OnOptionClickListener { + override fun onOptionClick(position: Int) { + val dialog = BottomSheetDialog(requireContext()) + val dialogView = BottomSheetQueueTrackOptionBinding.inflate(layoutInflater) + with(dialogView) { + btMoveUp.setOnClickListener { musicSource.moveItemUp(position) + queueAdapter.updateList(musicSource.catalogMetadata) + dialog.dismiss() } + btMoveDown.setOnClickListener { musicSource.moveItemDown(position) + queueAdapter.updateList(musicSource.catalogMetadata) + dialog.dismiss() } + btDelete.setOnClickListener { musicSource.removeMediaItem(position) + queueAdapter.updateList(musicSource.catalogMetadata) + dialog.dismiss() } + } + if (musicSource.catalogMetadata.size > 1) { + when (position) { + 0 -> { + setEnabledAll(dialogView.btMoveUp, false) + setEnabledAll(dialogView.btMoveDown, true) + } + musicSource.catalogMetadata.size - 1 -> { + setEnabledAll(dialogView.btMoveUp, true) + setEnabledAll(dialogView.btMoveDown, false) + } + else -> { + setEnabledAll(dialogView.btMoveUp, true) + setEnabledAll(dialogView.btMoveDown, true) + } + } + } + else { + setEnabledAll(dialogView.btMoveUp, false) + setEnabledAll(dialogView.btMoveDown, false) + setEnabledAll(dialogView.btDelete, false) + } + dialog.setCancelable(true) + dialog.setContentView(dialogView.root) + dialog.show() + } + }) } private fun updateNowPlaying(){ viewModel.nowPlayingMediaItem.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/DownloadedViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/DownloadedViewModel.kt index c92edc3b..4459634f 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/DownloadedViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/DownloadedViewModel.kt @@ -1,10 +1,13 @@ package com.maxrave.simpmusic.viewModel import android.app.Application +import android.widget.Toast import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.maxrave.simpmusic.common.DownloadState +import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity import com.maxrave.simpmusic.data.db.entities.SongEntity import com.maxrave.simpmusic.data.repository.MainRepository import dagger.hilt.android.lifecycle.HiltViewModel @@ -16,6 +19,9 @@ class DownloadedViewModel @Inject constructor(application: Application, private private var _listDownloadedSong: MutableLiveData> = MutableLiveData() val listDownloadedSong: LiveData> get() = _listDownloadedSong + private var _listLocalPlaylist: MutableLiveData> = MutableLiveData() + val localPlaylist: LiveData> = _listLocalPlaylist + private var _songEntity: MutableLiveData = MutableLiveData() val songEntity: LiveData = _songEntity @@ -49,4 +55,33 @@ class DownloadedViewModel @Inject constructor(application: Application, private getListDownloadedSong() } } + + fun getAllLocalPlaylist() { + viewModelScope.launch { + mainRepository.getAllLocalPlaylists().collect { values -> + _listLocalPlaylist.postValue(values) + } + } + } + + fun updateLocalPlaylistTracks(list: List, id: Long) { + viewModelScope.launch { + mainRepository.getSongsByListVideoId(list).collect { values -> + var count = 0 + values.forEach { song -> + if (song.downloadState == DownloadState.STATE_DOWNLOADED){ + count++ + } + } + mainRepository.updateLocalPlaylistTracks(list, id) + Toast.makeText(getApplication(), "Added to playlist", Toast.LENGTH_SHORT).show() + if (count == values.size) { + mainRepository.updateLocalPlaylistDownloadState(DownloadState.STATE_DOWNLOADED, id) + } + else { + mainRepository.updateLocalPlaylistDownloadState(DownloadState.STATE_NOT_DOWNLOADED, id) + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/LibraryViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/LibraryViewModel.kt index 0c468476..b7bc27d6 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/LibraryViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/LibraryViewModel.kt @@ -122,6 +122,7 @@ class LibraryViewModel @Inject constructor(private val mainRepository: MainRepos viewModelScope.launch { val localPlaylistEntity = LocalPlaylistEntity(title = title) mainRepository.insertLocalPlaylist(localPlaylistEntity) + getLocalPlaylist() } } diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/SettingsViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/SettingsViewModel.kt index 9bfbae0f..4a6d4946 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/SettingsViewModel.kt @@ -1,6 +1,10 @@ package com.maxrave.simpmusic.viewModel import android.app.Application +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.util.Log import android.widget.Toast import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData @@ -8,20 +12,39 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.cache.SimpleCache +import com.maxrave.simpmusic.R +import com.maxrave.simpmusic.common.DB_NAME import com.maxrave.simpmusic.common.DownloadState +import com.maxrave.simpmusic.common.QUALITY +import com.maxrave.simpmusic.common.SETTINGS_FILENAME import com.maxrave.simpmusic.data.dataStore.DataStoreManager +import com.maxrave.simpmusic.data.db.DatabaseDao +import com.maxrave.simpmusic.data.db.MusicDatabase import com.maxrave.simpmusic.data.repository.MainRepository import com.maxrave.simpmusic.di.DownloadCache import com.maxrave.simpmusic.di.PlayerCache +import com.maxrave.simpmusic.extension.div +import com.maxrave.simpmusic.extension.zipInputStream +import com.maxrave.simpmusic.extension.zipOutputStream +import com.maxrave.simpmusic.service.SimpleMediaService +import com.maxrave.simpmusic.ui.MainActivity import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.zip.ZipEntry import javax.inject.Inject +import kotlin.system.exitProcess @HiltViewModel class SettingsViewModel @Inject constructor( application: Application, private var dataStoreManager: DataStoreManager, private var mainRepository: MainRepository, + private var database: MusicDatabase, + private var databaseDao: DatabaseDao, @PlayerCache private val playerCache: SimpleCache, @DownloadCache private val downloadCache: SimpleCache ) : AndroidViewModel(application) { @@ -43,6 +66,29 @@ class SettingsViewModel @Inject constructor( getLocation() } } + private var _quality: MutableLiveData = MutableLiveData() + val quality: LiveData = _quality + + fun getQuality() { + viewModelScope.launch { + dataStoreManager.quality.collect { quality -> + when (quality) { + QUALITY.items[0].toString() -> _quality.postValue(QUALITY.items[0].toString()) + QUALITY.items[1].toString() -> _quality.postValue(QUALITY.items[1].toString()) + } + } + } + } + + fun changeQuality(checkedIndex: Int) { + viewModelScope.launch { + when (checkedIndex) { + 0 -> dataStoreManager.setQuality(QUALITY.items[0].toString()) + 1 -> dataStoreManager.setQuality(QUALITY.items[1].toString()) + } + getQuality() + } + } private val _cacheSize: MutableLiveData = MutableLiveData() var cacheSize: LiveData = _cacheSize @@ -86,4 +132,70 @@ class SettingsViewModel @Inject constructor( _cacheSize.value = playerCache.cacheSpace } } + + fun backup(context: Context, uri: Uri) { + kotlin.runCatching { + context.applicationContext.contentResolver.openOutputStream(uri)?.use { + it.buffered().zipOutputStream().use { outputStream -> +// (context.filesDir / "datastore" / SETTINGS_FILENAME).inputStream().buffered() +// .use { inputStream -> +// outputStream.putNextEntry(ZipEntry(SETTINGS_FILENAME)) +// inputStream.copyTo(outputStream) +// } + runBlocking(Dispatchers.IO) { + databaseDao.checkpoint() + } + FileInputStream(database.openHelper.writableDatabase.path).use { inputStream -> + outputStream.putNextEntry(ZipEntry(DB_NAME)) + inputStream.copyTo(outputStream) + } + } + } + }.onSuccess { + Toast.makeText(context, "Backup Create Success", Toast.LENGTH_SHORT).show() + }.onFailure { + it.printStackTrace() + Toast.makeText(context, "Backup Create Failed", Toast.LENGTH_SHORT).show() + } + } + + @UnstableApi + fun restore(context: Context, uri: Uri) { + runCatching { + context.applicationContext.contentResolver.openInputStream(uri)?.use { + it.zipInputStream().use { inputStream -> + var entry = inputStream.nextEntry + while (entry != null) { + when (entry.name) { +// SETTINGS_FILENAME -> { +// (context.filesDir / "datastore" / SETTINGS_FILENAME).outputStream().use { outputStream -> +// inputStream.copyTo(outputStream) +// } +// } + + DB_NAME -> { + runBlocking(Dispatchers.IO) { + databaseDao.checkpoint() + } + database.close() + FileOutputStream(database.openHelper.writableDatabase.path).use { outputStream -> + inputStream.copyTo(outputStream) + } + } + } + entry = inputStream.nextEntry + } + } + } + runBlocking { dataStoreManager.restore(true)} + context.stopService(Intent(context, SimpleMediaService::class.java)) + context.startActivity(Intent(context, MainActivity::class.java)) + exitProcess(0) + }.onSuccess { + Toast.makeText(context, context.getString(R.string.restore_success), Toast.LENGTH_SHORT).show() + }.onFailure { + it.printStackTrace() + Toast.makeText(context, context.getString(R.string.restore_failed), Toast.LENGTH_SHORT).show() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt index 323b476a..599f0018 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt @@ -2,6 +2,7 @@ package com.maxrave.simpmusic.viewModel import android.app.Application +import android.content.Intent import android.graphics.drawable.GradientDrawable import android.util.Log import android.widget.Toast @@ -13,21 +14,30 @@ import androidx.lifecycle.viewModelScope import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.cache.SimpleCache import androidx.media3.exoplayer.offline.Download import com.maxrave.simpmusic.common.Config import com.maxrave.simpmusic.common.DownloadState +import com.maxrave.simpmusic.common.QUALITY import com.maxrave.simpmusic.data.dataStore.DataStoreManager +import com.maxrave.simpmusic.data.dataStore.DataStoreManager.Settings.TRUE import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity import com.maxrave.simpmusic.data.db.entities.SongEntity import com.maxrave.simpmusic.data.model.browse.album.Track +import com.maxrave.simpmusic.data.model.browse.artist.ChannelId import com.maxrave.simpmusic.data.model.metadata.Line import com.maxrave.simpmusic.data.model.metadata.Lyrics import com.maxrave.simpmusic.data.model.metadata.MetadataSong import com.maxrave.simpmusic.data.model.searchResult.videos.VideosResult +import com.maxrave.simpmusic.data.model.songfull.SongFull import com.maxrave.simpmusic.data.queue.Queue import com.maxrave.simpmusic.data.repository.MainRepository +import com.maxrave.simpmusic.di.DownloadCache +import com.maxrave.simpmusic.di.PlayerCache import com.maxrave.simpmusic.extension.connectArtists import com.maxrave.simpmusic.extension.toListName +import com.maxrave.simpmusic.extension.toLyrics +import com.maxrave.simpmusic.extension.toLyricsEntity import com.maxrave.simpmusic.extension.toSongEntity import com.maxrave.simpmusic.service.PlayerEvent import com.maxrave.simpmusic.service.RepeatState @@ -42,6 +52,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -53,7 +64,7 @@ import javax.inject.Inject @HiltViewModel @UnstableApi -class SharedViewModel @Inject constructor(private var dataStoreManager: DataStoreManager, private val musicSource: MusicSource, private val mainRepository: MainRepository, private val simpleMediaServiceHandler: SimpleMediaServiceHandler, application: Application) : AndroidViewModel(application){ +class SharedViewModel @Inject constructor(private var dataStoreManager: DataStoreManager, @DownloadCache private val downloadedCache: SimpleCache, private val musicSource: MusicSource, private val mainRepository: MainRepository, private val simpleMediaServiceHandler: SimpleMediaServiceHandler, application: Application) : AndroidViewModel(application){ @Inject lateinit var downloadUtils: DownloadUtils @@ -130,9 +141,14 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor val repeatMode: StateFlow = _repeatMode private var regionCode: String? = null + private var quality: String? = null + private var isRestoring = MutableStateFlow(false) + + val intent: MutableStateFlow = MutableStateFlow(null) init { regionCode = runBlocking { dataStoreManager.location.first() } + quality = runBlocking { dataStoreManager.quality.first() } viewModelScope.launch { val job1 = launch { simpleMediaServiceHandler.simpleMediaState.collect { mediaState -> @@ -165,7 +181,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } val job2 = launch { - simpleMediaServiceHandler.changeTrack.collect { isChanged -> + simpleMediaServiceHandler.changeTrack.collectLatest { isChanged -> Log.d("Check Change Track", "Change Track: $isChanged") if (isChanged){ if (simpleMediaServiceHandler.getCurrentMediaItem()?.mediaId != videoId.value && simpleMediaServiceHandler.getCurrentMediaItem() != null){ @@ -189,7 +205,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor mainRepository.updateSongInLibrary(LocalDateTime.now(), tempSong.videoId) mainRepository.updateListenCount(tempSong.videoId) resetLyrics() - getLyrics(song.mediaMetadata.title.toString() + " " + song.mediaMetadata.artist) + getLyrics(song.mediaMetadata.title.toString() + " " + song.mediaMetadata.artist, song.mediaId) } } } @@ -266,20 +282,62 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } - fun getLyrics(query: String) { + fun checkIsRestoring() { + viewModelScope.launch { + dataStoreManager.isRestoringDatabase.first().let { restoring -> + isRestoring.value = restoring == TRUE + isRestoring.collect() { it -> + if (it) { + mainRepository.getDownloadedSongs().collect { songs -> + songs.forEach { song -> + if (!downloadedCache.keys.contains(song.videoId)) { + mainRepository.updateDownloadState(song.videoId, DownloadState.STATE_NOT_DOWNLOADED) + } + } + withContext(Dispatchers.Main) { + dataStoreManager.restore(false) + isRestoring.value = false + } + } + } + } + } + } + } + fun getLyrics(query: String, videoId: String) { viewModelScope.launch { mainRepository.getLyrics(query).collect { response -> _lyrics.value = response withContext(Dispatchers.Main){ - when(_lyrics.value) { - is Resource.Success -> { - parseLyrics(_lyrics.value?.data) - Log.d("Check Lyrics", _lyrics.value?.data.toString()) + if (_lyrics.value != null) { + Log.d("Check Lyrics", _lyrics.value.toString()) + when(_lyrics.value) { + is Resource.Success -> { + mainRepository.insertLyrics(_lyrics.value?.data!!.toLyricsEntity(videoId)) + parseLyrics(_lyrics.value?.data) + Log.d("Check Lyrics", _lyrics.value?.data.toString()) + } + else -> { + Log.d("Check Lyrics", "Get from DB") + mainRepository.getSavedLyrics(videoId).collect { lyrics -> + Log.d("Check Lyrics In DB", lyrics.toString()) + if (lyrics != null) { + _lyrics.value = Resource.Success(lyrics.toLyrics()) + val lyricsData = lyrics.toLyrics() + Log.d("Check Lyrics In DB", lyricsData.toString()) + parseLyrics(lyricsData) + } + } + } } - is Resource.Error -> { - + } + else { + Log.d("Check Lyrics", "null") + mainRepository.getSavedLyrics(videoId).collect { lyrics -> + if (lyrics != null) { + parseLyrics(lyrics.toLyrics()) + } } - else -> {} } } } @@ -327,6 +385,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } @UnstableApi fun loadMediaItemFromTrack(track: Track){ + quality = runBlocking { dataStoreManager.quality.first() } viewModelScope.launch { _firstTrackAdded.value = false simpleMediaServiceHandler.clearMediaItems() @@ -371,8 +430,18 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor when (values) { is Resource.Success -> { val listAudioStream = values.data + var itag = 0 + when (quality){ + QUALITY.items[0].toString() -> { + itag = QUALITY.itags[0] + } + QUALITY.items[1].toString() -> { + itag = QUALITY.itags[1] + } + } listAudioStream?.forEach { - if (it.itag == 251){ + if (it.itag == itag){ + Log.d("ITAG", it.itag.toString()) uri = it.url val artistName: String = track.artists.toListName().connectArtists() var thumbUrl = track.thumbnails?.last()?.url!! @@ -512,7 +581,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } - fun parseLyrics(lyrics: Lyrics?){ + private fun parseLyrics(lyrics: Lyrics?){ if (lyrics != null){ if (!lyrics.error){ if (lyrics.syncType == "LINE_SYNCED") @@ -568,19 +637,6 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor fun getLyricsString(current: Long): LyricDict? { -// viewModelScope.launch { -// while (isPlaying.value == true){ -// val lyric = lyricsFormat.value?.firstOrNull { it.startTimeMs.toLong() <= progressMillis.value!! } -// lyricsString.postValue(lyric?.words ?: "") -// delay(100) -// } -// } -// return if (lyricsFormat.value != null){ -// val lyric = lyricsFormat.value?.firstOrNull { it.startTimeMs.toLong() <= current } -// (lyric?.words ?: "") -// } else { -// "" -// } var listLyricDict: LyricDict? = null for (i in 0 until lyricsFormat.value?.size!!) { val sentence = lyricsFormat.value!![i] @@ -687,6 +743,36 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } } + + fun changeAllDownloadingToError() { + viewModelScope.launch { + mainRepository.getDownloadingSongs().collect {songs -> + songs.forEach { song -> + mainRepository.updateDownloadState(song.videoId, DownloadState.STATE_NOT_DOWNLOADED) + } + } + } + } + private val _songFull: MutableLiveData> = MutableLiveData() + var songFull: LiveData> = _songFull + + fun getSongFull(videoId: String) { + viewModelScope.launch { + mainRepository.getSongFull(videoId).collect { + _songFull.postValue(it) + } + } + } + + val _artistId: MutableLiveData> = MutableLiveData() + var artistId: LiveData> = _artistId + fun convertNameToId(artistId: String) { + viewModelScope.launch { + mainRepository.convertNameToId(artistId).collect { + _artistId.postValue(it) + } + } + } } sealed class UIEvent { object PlayPause : UIEvent() diff --git a/app/src/main/res/drawable/baseline_album_24.xml b/app/src/main/res/drawable/baseline_album_24.xml new file mode 100644 index 00000000..e6ec1df4 --- /dev/null +++ b/app/src/main/res/drawable/baseline_album_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_keyboard_double_arrow_down_24.xml b/app/src/main/res/drawable/baseline_keyboard_double_arrow_down_24.xml new file mode 100644 index 00000000..8044a0ce --- /dev/null +++ b/app/src/main/res/drawable/baseline_keyboard_double_arrow_down_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_keyboard_double_arrow_up_24.xml b/app/src/main/res/drawable/baseline_keyboard_double_arrow_up_24.xml new file mode 100644 index 00000000..be4fa522 --- /dev/null +++ b/app/src/main/res/drawable/baseline_keyboard_double_arrow_up_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_music_note_24.xml b/app/src/main/res/drawable/baseline_music_note_24.xml new file mode 100644 index 00000000..a6b3c2c6 --- /dev/null +++ b/app/src/main/res/drawable/baseline_music_note_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_search_24.xml b/app/src/main/res/drawable/baseline_search_24.xml new file mode 100644 index 00000000..e2dd96c6 --- /dev/null +++ b/app/src/main/res/drawable/baseline_search_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/logo_simpmusic_01.xml b/app/src/main/res/drawable/logo_simpmusic_01.xml new file mode 100644 index 00000000..4c2faad4 --- /dev/null +++ b/app/src/main/res/drawable/logo_simpmusic_01.xml @@ -0,0 +1,1630 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/logo_simpmusic_01_removebg_preview.xml b/app/src/main/res/drawable/logo_simpmusic_01_removebg_preview.xml new file mode 100644 index 00000000..a6e02f7c --- /dev/null +++ b/app/src/main/res/drawable/logo_simpmusic_01_removebg_preview.xml @@ -0,0 +1,1025 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 78613cee..91edeb57 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -18,120 +18,165 @@ - - - + + + + + + + + + + - + - - - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + android:layout_marginTop="3sp" + android:layout_marginStart="10sp" + android:layout_marginEnd="6sp" + android:max="100" + android:min="0" + android:progress="60" + app:indicatorColor="#B2FFFFFF" + app:trackColor="@android:color/transparent" + app:trackCornerRadius="3sp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_now_playing.xml b/app/src/main/res/layout/fragment_now_playing.xml index 90484940..c243643a 100644 --- a/app/src/main/res/layout/fragment_now_playing.xml +++ b/app/src/main/res/layout/fragment_now_playing.xml @@ -39,15 +39,16 @@ android:layout_below="@+id/topAppBarLayout"> diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 0cd64602..51429d7b 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -38,34 +38,32 @@ - - + android:layout_marginBottom="125sp"> - + - - - - + + + + android:orientation="vertical" + android:paddingBottom="180sp"> + android:text=""> + + + + + + + + + @@ -103,7 +132,7 @@ android:layout_height="wrap_content" android:textSize="16sp" android:id="@+id/tvPlayerCache" - android:text="60 MB"> + android:text=""> @@ -131,10 +160,102 @@ android:layout_height="wrap_content" android:textSize="16sp" android:id="@+id/tvDownloadedCache" - android:text="60 MB"> + android:text=""> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:text="v0.0.2-beta"> diff --git a/app/src/main/res/layout/item_queue_track.xml b/app/src/main/res/layout/item_queue_track.xml index 19537e35..7cd17c07 100644 --- a/app/src/main/res/layout/item_queue_track.xml +++ b/app/src/main/res/layout/item_queue_track.xml @@ -13,7 +13,7 @@ android:id="@+id/tvPosition" android:layout_width="40sp" android:layout_height="40sp" - android:text="1" + android:text="" android:gravity="center" android:fontFamily="@font/roboto" android:textStyle="bold" @@ -30,6 +30,20 @@ app:lottie_loop="true"> + + + + android:layout_toStartOf="@+id/btMore"> @@ -110,9 +109,6 @@ android:textSize="18sp" android:textStyle="bold" android:textColor="@android:color/white" /> - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_bottom_navigation.xml b/app/src/main/res/navigation/nav_bottom_navigation.xml index af7d15fc..0fbd89a9 100644 --- a/app/src/main/res/navigation/nav_bottom_navigation.xml +++ b/app/src/main/res/navigation/nav_bottom_navigation.xml @@ -189,5 +189,26 @@ android:id="@+id/infoFragment" android:name="com.maxrave.simpmusic.ui.fragment.player.InfoFragment" android:label="InfoFragment" /> + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index cecfc4d9..5b57e650 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -66,4 +66,6 @@ #7E878383 #FFFFFF + + #00FFFFFF \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 39e0b8b4..2b9c7f82 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,6 +34,15 @@ Recently Added Search for songs, artists, albums, playlists, and more Everything you need + Restore Failed + Restore Success + Search + Songs + Albums + Playlists + Artists + Library + Home %d song %d songs diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 00000000..8186d174 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml new file mode 100644 index 00000000..54ed7d68 --- /dev/null +++ b/app/src/main/res/xml/shortcuts.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 6350f07e..00000000 --- a/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -plugins { - id 'com.android.application' version '8.1.0' apply false - id 'com.android.library' version '8.1.0' apply false - id 'org.jetbrains.kotlin.android' version '1.8.20' apply false - id 'androidx.navigation.safeargs.kotlin' version '2.5.3' apply false - id 'com.google.dagger.hilt.android' version '2.45' apply false -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..37625a16 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,8 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id("com.android.application") version "8.1.0" apply false + id("com.android.library") version "8.1.0" apply false + id("org.jetbrains.kotlin.android") version "1.8.20" apply false + id("androidx.navigation.safeargs.kotlin") version "2.5.3" apply false + id("com.google.dagger.hilt.android") version "2.45" apply false +} diff --git a/fastlane/metadata/android/en-US/changelogs/2.txt b/fastlane/metadata/android/en-US/changelogs/2.txt new file mode 100644 index 00000000..b2aba836 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/2.txt @@ -0,0 +1,17 @@ +Fixes: + +- Fixed maxrave-dev/SimpMusic#4 +- Fixed bugs +- Migrate Gradle to Kotlin KTS + +New Feature: + +- Remove Thumbnail Cache +- Move or remove Queue Track +- Backup and restore data (maxrave-dev/SimpMusic#2) +- Add swipe to remove miniplayer +- Add SimpMusic to YouTube, YouTube Music's share menu (maxrave-dev/SimpMusic#9) +- Open YouTube link by default +- Add static shortcuts +- Auto save lyrics for offline playback +- Change Quality \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 06508f3e..35b86e2d 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -4,6 +4,7 @@ A simple music app using YouTube Music for backend - Play music from YouTube Music or YouTube free without ads in the background - Browsing Home, Charts, Moods & Genre with YouTube Music data +- Browse and play all contents blocked in your country without VPN or Proxy - Search everything on YouTube - Analyze your playing data and create custom playlists ... - Caching and can save data for offline playback diff --git a/gradle.properties b/gradle.properties index 3344508f..3b4eb2be 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,6 +15,7 @@ org.gradle.jvmargs=-Xmx4608m # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true +android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the @@ -24,6 +25,5 @@ android.nonTransitiveRClass=true android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=false - kotlin.jvm.target.validation.mode = IGNORE org.gradle.unsafe.configuration-cache=true \ No newline at end of file diff --git a/settings.gradle b/settings.gradle.kts similarity index 82% rename from settings.gradle rename to settings.gradle.kts index 384f1976..3ee72b59 100644 --- a/settings.gradle +++ b/settings.gradle.kts @@ -10,8 +10,8 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven { url "https://jitpack.io" } + maven { url = uri("https://jitpack.io") } } } rootProject.name = "SimpMusic" -include ':app' +include ("app")