diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..7b135405 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,54 @@ +name: Bug Report +description: Create a report to help us improve +title: "[Bug Report]" +labels: [bug] +body: + + + + - type: textarea + attributes: + label: Describe the bug + description: + placeholder: | + A clear and concise description of what the bug is. + validations: + required: false + + - type: textarea + attributes: + label: To Reproduce + placeholder: | + Steps to reproduce the behavior: + 1.Go to '...' + 2.Click on '....' + 3.Scroll down to '....' + 4.See error + validations: + required: false + + - type: textarea + attributes: + label: Error reports & Screenshots + placeholder: | + When the error happened, click at the bottom of the page on Copy Details and paste it here. + validations: + required: false + + + - type: textarea + attributes: + label: Device info + description: | + Please provide some information of the device you are using. + placeholder: | + App version, OS version, device model, etc... + validations: + required: false + + - type: textarea + attributes: + label: Additional context + description: + placeholder: | + Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..26814da6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,41 @@ +name: Feature Request +description: Suggest a new feature for Jetispot +title: "[Feature Request]" +labels: [enhancement] +body: + - type: checkboxes + id: checklist + attributes: + label: Checklist + description: | + Even if you're not sure about the answer, feel free to leave it blank and provide us with more information about this request. + options: + - label: This feature is merely a UI/UX update. + required: false + - label: This feature is not going to conflict with many of the existing options. + required: false + - type: textarea + id: description_1 + attributes: + label: Is your feature request related to a problem? Please describe. + description: + placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: false + - type: textarea + id: description_2 + attributes: + label: Describe the solution you'd like + description: + placeholder: A clear and concise description of what you want to happen. + validations: + required: false + - type: textarea + id: description_4 + attributes: + label: Additional context + description: + placeholder: Add any other context or screenshots about the feature request here. + validations: + required: false + render: shell \ No newline at end of file diff --git a/.gitignore b/.gitignore index aa724b77..dc81aca2 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ .externalNativeBuild .cxx local.properties +/.idea/.name +/app/release/ +/jetispotkeypass.jks diff --git a/README.md b/README.md index 702102e7..61232a12 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,59 @@ -## Jetispot -_probably usable __UNOFFICIAL__ Spotify client for Android, built with Jetpack Compose and librespot-java_ +
+
+

Jetispot

-#### Spotify Premium account is REQUIRED*. Offline caching, DRM bypassing or raw file downloading is prohibted by ToS and will NEVER be implemented in Jetispot. Don't waste your time trying to request these features. +
+ +A Spotify unofficial client built with Jetpack Compose, Material You/3 and librespot-java -__What's working:__ -- sign in (login/pass only, no FB/Meta/whatsoever support, no Smart Lock either) -- "browse", "home", album, premium plan, artist and genre screens (some of the blocks might be unsupported) -- library: "liked songs" w/ tag&sort support, rootlist (liked playlists) + pins + artist/album support w/ nice animations, delta updates + pub/sub processing support -- basic playback w/ Spotify Connect support (connect support is very WIP) -- fairly optimized R8 rules, providing __approx. 5-6 megabytes__ release APK size (with the playback and protobuf parts!) +
-__What's in progress:__ +## 📣 NOTICE +Spotify Premium account is **REQUIRED***. Offline caching, DRM bypassing or raw file downloading is prohibted by ToS and will NEVER be implemented in Jetispot. Don't waste your time trying to request these features. + +## 🔮 App features +- Sign In (login/pass only, no FB/Meta/Google support, no Smart Lock either) +- "Browse", "Home", Album, Premium Plans overview, Artists and Genres screens (some of the blocks might be unsupported). +- Library: "liked songs" with tag & sort support, rootlist (liked playlists) + pins + artist/album support with nice animations, delta updates and also pub/sub processing support +- Basic playback with Spotify Connect support (Spotify Connect support is actually WIP) +- Fairly optimized R8 rules, providing the release APKs with a size of 4-5mb (with the playback and protobuf parts!) + +## 📸 Screentshots + +
+image +image +image +image +image +image +
+ + +## 🔨 What's in progress - "Now Playing" improvements -- better service (notification improvements) +- Better playback service (notification improvements) +- Fixing "unsupported" warnings -__Application stack:__ -- playback: librespot-java as the core + sinks/decoders from librespot-android + Media2 for the mediasession support -- UI: Jetpack Compose +## 👷 App specifications +- Playback: librespot-java as the core + sinks/decoders from librespot-android + Media2 for the mediasession support +- UI: Jetpack Compose with Material You - DI: Hilt/Dagger -- network: Retrofit w/ Moshi+Protobuf converters +- network: Retrofit w/ Moshi + Protobuf converters - pictures: Coil - storage: Room (collection), MMKV (metadata) - arch: MVVM - preferences: Jetpack Datastore (proto) -__Credits:__ +## ⬇️ Downloads +You can go to the [releases page](https://github.com/BobbyESP/Jetispot/releases) and download any version updated. + +## Credits - [librespot-java](https://github.com/librespot-org/librespot-java) for the core API part and playback - [librespot-android](https://github.com/devgianlu/librespot-android) for sink and decoder source (in Jetispot they are rewritten to Kotlin) - [moshi](https://github.com/square/moshi/) and [moshix](https://github.com/ZacSweers/MoshiX/) for the undocumented API JSON parsing - [VK Icons](https://github.com/VKCOM/icons) for the amazing icon set used in the application's icon - [MMKV](https://github.com/Tencent/MMKV) for ultra-fast way to cache entity extended metadata -- Google for Android/Jetpack/Hilt +- Google for Jetpack Compose, Protocol Buffers and Material UI components -_* I heard some people can log in with a free account, but I won't provide any assistance to people without premium subscription. There is a possibility that a subscription check may be added to the client side in the future._ +_* Some people can actually login without an Spotify Premium account. Assistance to this accounts may be not be provided and you risk yourself for using a free acount. We will not be held responsible for any ban._ diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 7a9516d7..00000000 --- a/app/build.gradle +++ /dev/null @@ -1,163 +0,0 @@ -plugins { - id "com.android.application" - id "org.jetbrains.kotlin.android" - id "dev.zacsweers.moshix" version "0.18.3" - id "dagger.hilt.android.plugin" - id "kotlin-kapt" - id "com.google.protobuf" version "0.8.18" -} - -android { - compileSdk 33 - - defaultConfig { - applicationId "bruhcollective.itaysonlab.jetispot" - minSdk 23 - targetSdk 33 - versionCode version_code - versionName version_name - resConfigs 'en' - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - sourceSets.main { - jniLibs.srcDir "src/main/libs" - } - - buildTypes { - release { - minifyEnabled true - shrinkResources true - proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" - } - } - - compileOptions { - coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = "1.8" - freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" - } - - buildFeatures { - compose true - } - - composeOptions { - kotlinCompilerExtensionVersion compose_compiler_version - } - - kapt { - arguments { - arg("room.schemaLocation", "$projectDir/schemas") - } - } - - packagingOptions { - resources { - excludes += "/META-INF/*.kotlin_module" - excludes += "/META-INF/*.version" - excludes += "/META-INF/**" - excludes += "/kotlin/**" - excludes += "/kotlinx/**" - excludes += "**/*.properties" - excludes += "DebugProbesKt.bin" - excludes += "kotlin-tooling-metadata.json" - excludes += "/META-INF/{AL2.0,LGPL2.1}" - excludes += "log4j2.xml" - excludes += "**.proto" - } - } - namespace 'bruhcollective.itaysonlab.jetispot' -} - -moshi { - // Opt-in to enable moshi-sealed, disabled by default. - enableSealed.set(true) -} - -dependencies { - // Kotlin - implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0" - - // AndroidX - coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.2.0" - implementation "androidx.core:core-ktx:1.9.0" - implementation "androidx.palette:palette-ktx:1.0.0" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1" - - // Compose - implementation "androidx.navigation:navigation-compose:2.5.2" - implementation "androidx.activity:activity-compose:1.6.0" - implementation "androidx.compose.material:material:$compose_version" - implementation "androidx.compose.material3:material3:$compose_m3_version" - implementation "androidx.compose.material:material-icons-extended:$compose_version" - implementation "androidx.compose.ui:ui:$compose_version" - implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation "androidx.compose.ui:ui-util:$compose_version" - implementation "androidx.compose.ui:ui-tooling:$compose_version" - debugImplementation "androidx.customview:customview:1.2.0-alpha02" - debugImplementation "androidx.customview:customview-poolingcontainer:1.0.0" - - // Compose - Additions - implementation "com.google.accompanist:accompanist-navigation-material:$accompanist_version" - implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version" - implementation "com.google.accompanist:accompanist-pager:$accompanist_version" - - // Images - implementation "io.coil-kt:coil-compose:2.2.0" - - // DI - implementation "androidx.hilt:hilt-navigation-compose:1.0.0" - implementation "com.google.dagger:hilt-android:$hilt_version" - kapt "com.google.dagger:hilt-compiler:$hilt_version" - - // Playback - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.4" - implementation "com.gitlab.mvysny.slf4j:slf4j-handroid:1.7.30" - implementation "androidx.media2:media2-session:$media2_version" - implementation "androidx.media2:media2-player:$media2_version" - - // Librespot - implementation ("com.github.iTaysonLab.librespot-java:librespot-player:$librespot_commit:thin") { - exclude group: "xyz.gianlu.librespot", module: "librespot-sink" - exclude group: "com.lmax", module: "disruptor" - exclude group: "org.apache.logging.log4j" - } - - // Data - Network - implementation "com.squareup.retrofit2:retrofit:2.9.0" - implementation "com.squareup.retrofit2:converter-moshi:2.9.0" - implementation "com.squareup.retrofit2:converter-protobuf:2.9.0" - - // Data - SQL - implementation "androidx.room:room-runtime:$room_version" - implementation "androidx.room:room-ktx:$room_version" - implementation "androidx.room:room-paging:$room_version" - kapt "androidx.room:room-compiler:$room_version" - - // Data - Proto - implementation "androidx.datastore:datastore:1.0.0" - implementation "com.google.protobuf:protobuf-java:3.21.5" - implementation "com.tencent:mmkv:1.2.14" -} - -protobuf { - protoc { - artifact = "com.google.protobuf:protoc:3.21.5" - } - - generateProtoTasks { - all().each { task -> - task.builtins { - java { - //option "lite" - } - } - } - } -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..8d885c24 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,246 @@ +import java.io.FileInputStream +import java.util.Properties +import com.google.protobuf.gradle.* + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("dev.zacsweers.moshix") version "0.18.3" + id("dagger.hilt.android.plugin") + id("kotlin-kapt") + id("com.google.protobuf") version "0.8.18" + kotlin("plugin.serialization") version "1.7.21" +} +apply(plugin = "dagger.hilt.android.plugin") + +val versionMajor = 0 +val versionMinor = 1 +val versionPatch = 5 +val versionBuild = 0 +val isStable = true + +val compose_version: String by rootProject.extra +val compose_m3_version: String by rootProject.extra +val compose_compiler_version: String by rootProject.extra +val media2_version: String by rootProject.extra +val accompanist_version: String by rootProject.extra +val room_version: String by rootProject.extra +val librespot_commit: String by rootProject.extra +val hilt_version: String by rootProject.extra + +val keystorePropertiesFile = rootProject.file("keystore.properties") + +val splitApks = !project.hasProperty("noSplits") + +android { + if (keystorePropertiesFile.exists()) { + val keystoreProperties = Properties() + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) + signingConfigs { + getByName("debug") + { + keyAlias = keystoreProperties["keyAlias"].toString() + keyPassword = keystoreProperties["keyPassword"].toString() + storeFile = file(keystoreProperties["storeFile"]!!) + storePassword = keystoreProperties["storePassword"].toString() + } + } + } + + compileSdk = 33 + defaultConfig { + applicationId = "bruhcollective.itaysonlab.jetispot" + minSdk = 23 + targetSdk = 33 + versionCode = 1000 + versionName = StringBuilder("${versionMajor}.${versionMinor}.${versionPatch}").apply { + if (!isStable) append("-beta.${versionBuild}") + }.toString() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + kapt { + arguments { + arg("room.schemaLocation", "$projectDir/schemas") + } + } + if (!splitApks) + ndk { + (properties["ABI_FILTERS"] as String).split(';').forEach { + abiFilters.add(it) + } + } + } + if (splitApks) + splits { + abi { + isEnable = !project.hasProperty("noSplits") + reset() + include("arm64-v8a", "armeabi-v7a", "x86", "x86_64") + isUniversalApk = false + } + } + //source sets in .kts + sourceSets { + getByName("main") { + java.srcDir("src/main/libs") + } + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + if (keystorePropertiesFile.exists()) + signingConfig = signingConfigs.getByName("release") + matchingFallbacks.add(0, "debug") + matchingFallbacks.add(1, "release") + } + debug { + if (keystorePropertiesFile.exists()) + signingConfig = signingConfigs.getByName("debug") + matchingFallbacks.add(0, "debug") + matchingFallbacks.add(1, "release") + } + } + + compileOptions { + // coreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = compose_compiler_version + } + + applicationVariants.all { + outputs.all { + (this as com.android.build.gradle.internal.api.BaseVariantOutputImpl).outputFileName = + "Jetispot-${defaultConfig.versionName}-${name}.apk" + } + } + + lint { + disable.addAll(listOf("MissingTranslation", "ExtraTranslation")) + } + + packagingOptions { + resources { + excludes += "/META-INF/*.kotlin_module" + excludes += "/META-INF/*.version" + excludes += "/META-INF/**" + excludes += "/kotlin/**" + excludes += "/kotlinx/**" + excludes += "**/*.properties" + excludes += "DebugProbesKt.bin" + excludes += "kotlin-tooling-metadata.json" + excludes += "/META-INF/{AL2.0,LGPL2.1}" + excludes += "log4j2.xml" + excludes += "**.proto" + } + } + namespace = "bruhcollective.itaysonlab.jetispot" +} + +moshi { + // Opt-in to enable moshi-sealed, disabled by default. + enableSealed.set(true) +} + +dependencies { + // Kotlin + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") + + + // AndroidX + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.0") + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.palette:palette-ktx:1.0.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1") + implementation("androidx.appcompat:appcompat:1.7.0-alpha01") + + // Compose + implementation("androidx.navigation:navigation-compose:2.5.2") + implementation("androidx.activity:activity-compose:1.6.1") + implementation("androidx.compose.material:material:$compose_version") + implementation("androidx.compose.material3:material3:$compose_m3_version") + implementation("androidx.compose.material:material-icons-extended:$compose_version") + implementation("androidx.compose.ui:ui:$compose_version") + implementation("androidx.compose.ui:ui-tooling-preview:$compose_version") + implementation("androidx.compose.ui:ui-util:$compose_version") + implementation("androidx.compose.ui:ui-tooling:$compose_version") + debugImplementation("androidx.customview:customview:1.2.0-alpha02") + debugImplementation("androidx.customview:customview-poolingcontainer:1.0.0") + + // Compose - Additions + implementation("com.google.accompanist:accompanist-navigation-material:$accompanist_version") + implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanist_version") + implementation("com.google.accompanist:accompanist-pager:$accompanist_version") + + // Images + implementation("io.coil-kt:coil-compose:2.2.0") + + // DI + implementation("androidx.hilt:hilt-navigation-compose:1.0.0") + implementation("com.google.dagger:hilt-android:$hilt_version") + kapt("com.google.dagger:hilt-compiler:$hilt_version") + + // Playback + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.4") + implementation("com.gitlab.mvysny.slf4j:slf4j-handroid:1.7.30") + implementation("androidx.media2:media2-session:$media2_version") + implementation("androidx.media2:media2-player:$media2_version") + + // Librespot + implementation("com.github.iTaysonLab.librespot-java:librespot-player:$librespot_commit:thin") { + exclude(group = "xyz.gianlu.librespot", module = "librespot-sink") + exclude(group = "com.lmax", module = "disruptor") + exclude(group = "org.apache.logging.log4j") + } + + // Data - Network + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-moshi:2.9.0") + implementation("com.squareup.retrofit2:converter-protobuf:2.9.0") + + // Data - SQL + implementation("androidx.room:room-runtime:$room_version") + implementation("androidx.room:room-ktx:$room_version") + implementation("androidx.room:room-paging:$room_version") + kapt("androidx.room:room-compiler:$room_version") + + // Data - Proto + implementation("androidx.datastore:datastore:1.0.0") + implementation("com.google.protobuf:protobuf-java:3.21.5") + implementation("com.tencent:mmkv:1.2.14") +} + +//https://stackoverflow.com/questions/65390807/unresolved-reference-protoc-when-using-gradle-protocol-buffers +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.21.5" + } + + generateProtoTasks { + all().forEach { + it.builtins { + create("java") { + //option("lite") + } + } + } + } +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 7f9fc50f..f0b13da3 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 @@ -48,12 +48,40 @@ # genericjson gson fixes -keep,allowobfuscation class * extends xyz.gianlu.librespot.json.JsonWrapper { *; } +# Keep `Companion` object fields of serializable classes. +# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. +-if @kotlinx.serialization.Serializable class ** +-keepclassmembers class <1> { + static <1>$Companion Companion; +} + +# Keep `serializer()` on companion objects (both default and named) of serializable classes. +-if @kotlinx.serialization.Serializable class ** { + static **$* *; +} +-keepclassmembers class <2>$<3> { + kotlinx.serialization.KSerializer serializer(...); +} + +# Keep `INSTANCE.serializer()` of serializable objects. +-if @kotlinx.serialization.Serializable class ** { + public static ** INSTANCE; +} +-keepclassmembers class <1> { + public static <1> INSTANCE; + kotlinx.serialization.KSerializer serializer(...); +} + +# @Serializable and @Polymorphic are used at runtime for polymorphic serialization. +-keepattributes RuntimeVisibleAnnotations,AnnotationDefault + # protobuf -keep class com.spotify.** {*;} -keep class spotify.** {*;} -keep class bruhcollective.itaysonlab.swedentricks.** {*;} -keep class bruhcollective.itaysonlab.jetispot.proto.** {*;} -keep class com.google.protobuf.Any {*;} +-keep class com.google.protobuf.GeneratedMessageV3 {*;} -keep class * extends com.google.protobuf.AbstractMessage {*;} # librespot @@ -83,7 +111,6 @@ -keepclassmembers,allowshrinking,allowobfuscation interface * { @retrofit2.http.* ; } --dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -dontwarn javax.annotation.** -dontwarn kotlin.Unit -dontwarn retrofit2.KotlinExtensions @@ -92,4 +119,6 @@ -keep,allowobfuscation interface <1> -keep,allowobfuscation,allowshrinking interface retrofit2.Call -keep,allowobfuscation,allowshrinking class retrofit2.Response --keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation \ No newline at end of file +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation +-dontwarn kotlinx.serialization.KSerializer +-dontwarn kotlinx.serialization.Serializable \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c0f941f7..115c17e3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,8 @@ + + @@ -19,11 +21,12 @@ android:networkSecurityConfig="@xml/nsc" android:name=".SpApp" android:allowBackup="true" + android:localeConfig="@xml/locales_config" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.Jetispot" - tools:targetApi="n"> + tools:targetApi="tiramisu"> + + + + + + + + = Build.VERSION_CODES.N) { + updateResources(context, language) + } else updateResourcesLegacy(context, language) + } + + private fun getPersistedData(context: Context, defaultLanguage: String): String { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + return preferences.getString("Language", defaultLanguage)!! + } + + private fun persist(context: Context, language: String) { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + val editor = preferences.edit() + + editor.putString("Language", language) + editor.apply() + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun updateResources(context: Context, language: String): Context { + val locale = Locale(language) + Locale.setDefault(locale) + + val configuration = context.resources.configuration + configuration.setLocale(locale) + configuration.setLayoutDirection(locale) + + return context.createConfigurationContext(configuration) + } + + private fun updateResourcesLegacy(context: Context, language: String): Context { + val locale = Locale(language) + Locale.setDefault(locale) + + val resources = context.resources + + val configuration = resources.configuration + + val config = Configuration() + if (language.isEmpty()) { + val emptyLocaleList = LocaleListCompat.getEmptyLocaleList() + config.setLocale(emptyLocaleList[0]) + } else { + config.setLocale(locale) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + configuration.setLayoutDirection(locale) + } + + resources.updateConfiguration(configuration, resources.displayMetrics) + + return context + } + } +} diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/MainActivity.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/MainActivity.kt index b53d68e9..f98b5d2d 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/MainActivity.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/MainActivity.kt @@ -1,6 +1,9 @@ package bruhcollective.itaysonlab.jetispot +import android.content.Context import android.content.Intent +import android.content.res.Configuration +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.addCallback @@ -9,6 +12,7 @@ import androidx.activity.compose.setContent import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.BottomSheetScaffold import androidx.compose.material.BottomSheetValue import androidx.compose.material.ExperimentalMaterialApi @@ -16,16 +20,22 @@ import androidx.compose.material.rememberBottomSheetScaffoldState import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.core.os.LocaleListCompat import androidx.core.view.WindowCompat import androidx.navigation.NavController import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import bruhcollective.itaysonlab.jetispot.SpApp.Companion.applicationScope +import bruhcollective.itaysonlab.jetispot.SpApp.Companion.context import bruhcollective.itaysonlab.jetispot.core.SpAuthManager import bruhcollective.itaysonlab.jetispot.core.SpPlayerServiceManager import bruhcollective.itaysonlab.jetispot.core.SpSessionManager +import bruhcollective.itaysonlab.jetispot.core.util.Log import bruhcollective.itaysonlab.jetispot.ui.AppNavigation import bruhcollective.itaysonlab.jetispot.ui.ext.compositeSurfaceElevation import bruhcollective.itaysonlab.jetispot.ui.navigation.LocalNavigationController @@ -37,7 +47,11 @@ import com.google.accompanist.navigation.material.ExperimentalMaterialNavigation import com.google.accompanist.navigation.material.ModalBottomSheetLayout import com.google.accompanist.navigation.material.rememberBottomSheetNavigator import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.util.* import javax.inject.Inject @AndroidEntryPoint @@ -67,6 +81,9 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterialApi::class, ExperimentalMaterialNavigationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + runBlocking{ + updateLanguage(context, LocaleHelper.getLanguage(context)) + } WindowCompat.setDecorFitsSystemWindows(window, false) @@ -75,6 +92,11 @@ class MainActivity : ComponentActivity() { val backPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current // remembers val scope = rememberCoroutineScope() + + //q: can a remember bottom sheet scaffold state be used for multiple bottom sheets? + //a: yes, but you need to use the same scaffold state for all of them + + //In conclusion, you can have multiple bottom sheets, but you can only have one bottom sheet scaffold state val bsState = rememberBottomSheetScaffoldState() val bottomSheetNavigator = rememberBottomSheetNavigator() @@ -193,19 +215,21 @@ class MainActivity : ComponentActivity() { ) { innerPadding -> BottomSheetScaffold( sheetContent = { - NowPlayingScreen( - bottomSheetState = bsState.bottomSheetState, - bsOffset = bsOffset, - queueOpened = bsQueueOpened, - setQueueOpened = { bsQueueOpened = it }, - lyricsOpened = bsLyricsOpened, - setLyricsOpened = { bsLyricsOpened = it } - ) + NowPlayingScreen( + bottomSheetState = bsState.bottomSheetState, + bsOffset = bsOffset, + queueOpened = bsQueueOpened, + setQueueOpened = { bsQueueOpened = it }, + lyricsOpened = bsLyricsOpened, + setLyricsOpened = { bsLyricsOpened = it } + ) }, scaffoldState = bsState, sheetPeekHeight = bsPeek, backgroundColor = MaterialTheme.colorScheme.surface, - sheetGesturesEnabled = !bsQueueOpened && !bsLyricsOpened + sheetGesturesEnabled = !bsQueueOpened && !bsLyricsOpened, + sheetShape = RoundedCornerShape(36.dp * (1f - bsOffset())), + modifier = Modifier.background(Color.Transparent) ) { innerScaffoldPadding -> AppNavigation( navController = navController, @@ -222,4 +246,20 @@ class MainActivity : ComponentActivity() { } } } + companion object{ + + fun updateLanguage(context: Context = SpApp.context, language: String) { + val locale = Locale(language) + Locale.setDefault(locale) + val config = Configuration() + if (language.isEmpty()) { + val emptyLocaleList = LocaleListCompat.getEmptyLocaleList() + config.setLocale(emptyLocaleList[0]) + } else { + config.setLocale(locale) + } + + context.resources.updateConfiguration(config, context.resources.displayMetrics) + } + } } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/SpApp.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/SpApp.kt index 0f9d569f..57bb376b 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/SpApp.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/SpApp.kt @@ -1,10 +1,14 @@ package bruhcollective.itaysonlab.jetispot +import android.annotation.SuppressLint import android.app.Application +import android.content.Context import android.os.Build import bruhcollective.itaysonlab.jetispot.playback.sp.AndroidNativeDecoder import com.tencent.mmkv.MMKV import dagger.hilt.android.HiltAndroidApp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import org.slf4j.LoggerFactory import org.slf4j.impl.HandroidLoggerAdapter import xyz.gianlu.librespot.audio.decoders.Decoders @@ -24,6 +28,15 @@ class SpApp: Application() { override fun onCreate() { super.onCreate() + context = applicationContext + applicationScope = CoroutineScope(SupervisorJob()) MMKV.initialize(this, "${filesDir.absolutePath}/spa_meta") } + + companion object{ + lateinit var applicationScope: CoroutineScope + + @SuppressLint("StaticFieldLeak") + lateinit var context: Context + } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpConfigurationManager.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpConfigurationManager.kt index 31d5ab15..235f5f2d 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpConfigurationManager.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpConfigurationManager.kt @@ -5,6 +5,7 @@ import androidx.datastore.core.CorruptionException import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.Serializer +import bruhcollective.itaysonlab.jetispot.SpApp import bruhcollective.itaysonlab.jetispot.proto.AppConfig import bruhcollective.itaysonlab.jetispot.proto.AudioNormalization import bruhcollective.itaysonlab.jetispot.proto.AudioQuality @@ -23,9 +24,11 @@ import javax.inject.Singleton @Singleton class SpConfigurationManager @Inject constructor( - @ApplicationContext private val appContext: Context + @ApplicationContext private val appContext: Context, ) { companion object { + //get spSessionManager session + private val spSessionManager = SpSessionManager(SpApp.context) val EMPTY = object: DataStore { override val data: Flow get() = emptyFlow() override suspend fun updateData(transform: suspend (t: AppConfig) -> AppConfig) = TODO("This is an empty DataStore!") @@ -35,7 +38,7 @@ class SpConfigurationManager @Inject constructor( setPlayerConfig(PlayerConfig.newBuilder().apply { autoplay = true normalization = true - preferredQuality = AudioQuality.VERY_HIGH + preferredQuality = AudioQuality.HIGH normalizationLevel = AudioNormalization.BALANCED crossfade = 0 preload = true diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpMetadataRequester.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpMetadataRequester.kt index a7dbc326..dccf8393 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpMetadataRequester.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpMetadataRequester.kt @@ -16,109 +16,138 @@ import javax.inject.Singleton @Singleton class SpMetadataRequester @Inject constructor( - private val spSessionManager: SpSessionManager, - private val spMetadataDb: SpMetadataDb + private val spSessionManager: SpSessionManager, + private val spMetadataDb: SpMetadataDb ) { - companion object { - private val coreKinds = arrayOf( - ExtensionKindOuterClass.ExtensionKind.EPISODE_V4, - ExtensionKindOuterClass.ExtensionKind.SHOW_V4, - ExtensionKindOuterClass.ExtensionKind.ARTIST_V4, - ExtensionKindOuterClass.ExtensionKind.ALBUM_V4, - ExtensionKindOuterClass.ExtensionKind.TRACK_V4, - ExtensionKindOuterClass.ExtensionKind.USER_PROFILE - ) - } - - private inline fun MutableMap.runOrAdd(key: K, ifExists: (V) -> Unit, ifNotExists: () -> V) { - val value = get(key) - if (value != null) { - ifExists(value) - } else { - put(key, ifNotExists()) + companion object { + private val coreKinds = arrayOf( + ExtensionKindOuterClass.ExtensionKind.EPISODE_V4, + ExtensionKindOuterClass.ExtensionKind.SHOW_V4, + ExtensionKindOuterClass.ExtensionKind.ARTIST_V4, + ExtensionKindOuterClass.ExtensionKind.ALBUM_V4, + ExtensionKindOuterClass.ExtensionKind.TRACK_V4, + ExtensionKindOuterClass.ExtensionKind.USER_PROFILE + ) } - } - - suspend fun request(builder: MutableRequestEntities.() -> Unit): UnpackedMetadataResponse = request(buildList(builder)) - suspend fun request(entities: RequestEntities) = withContext(Dispatchers.IO) { - val result = UnpackedMetadataResponse(emptyList()) - if (entities.isEmpty()) return@withContext result - - val alreadyCached = mutableMapOf() - val shouldRequest = mutableMapOf() - - entities.forEach { entity -> - val uri = entity.first - entity.second.forEach { kind -> - val kindInDbUri = if (coreKinds.contains(kind)) uri else "$uri:${kind.ordinal}" - if (spMetadataDb.contains(kindInDbUri)) { - val extensionData = EntityExtensionDataOuterClass.EntityExtensionData.newBuilder().setEntityUri(uri).setExtensionData(Any.newBuilder().setValue(ByteString.copyFrom(spMetadataDb.get(kindInDbUri))).build()).build() - alreadyCached.runOrAdd(kind, ifExists = { - it.addExtensionData(extensionData) - }, ifNotExists = { - ExtendedMetadata.EntityExtensionDataArray.newBuilder().setExtensionKind(kind).addExtensionData(extensionData) - }) + + private inline fun MutableMap.runOrAdd( + key: K, + ifExists: (V) -> Unit, + ifNotExists: () -> V + ) { + val value = get(key) + if (value != null) { + ifExists(value) } else { - val query = ExtendedMetadata.ExtensionQuery.newBuilder().setExtensionKind(kind).build() - shouldRequest.runOrAdd(uri, ifExists = { - it.addQuery(query) - }, ifNotExists = { - ExtendedMetadata.EntityRequest.newBuilder().setEntityUri(uri).addQuery(query) - }) + put(key, ifNotExists()) } - } } - result += UnpackedMetadataResponse(alreadyCached.map { it.value.build() }) + suspend fun request(builder: MutableRequestEntities.() -> Unit): UnpackedMetadataResponse = + request(buildList(builder)) + + suspend fun request(entities: RequestEntities) = withContext(Dispatchers.IO) { + val result = UnpackedMetadataResponse(emptyList()) + if (entities.isEmpty()) return@withContext result + + val alreadyCached = + mutableMapOf() + val shouldRequest = mutableMapOf() + + entities.forEach { entity -> + val uri = entity.first + entity.second.forEach { kind -> + val kindInDbUri = if (coreKinds.contains(kind)) uri else "$uri:${kind.ordinal}" + if (spMetadataDb.contains(kindInDbUri)) { + val extensionData = + EntityExtensionDataOuterClass.EntityExtensionData.newBuilder() + .setEntityUri(uri).setExtensionData( + Any.newBuilder() + .setValue(ByteString.copyFrom(spMetadataDb.get(kindInDbUri))) + .build() + ).build() + alreadyCached.runOrAdd(kind, ifExists = { + it.addExtensionData(extensionData) + }, ifNotExists = { + ExtendedMetadata.EntityExtensionDataArray.newBuilder() + .setExtensionKind(kind).addExtensionData(extensionData) + }) + } else { + val query = + ExtendedMetadata.ExtensionQuery.newBuilder().setExtensionKind(kind).build() + shouldRequest.runOrAdd(uri, ifExists = { + it.addQuery(query) + }, ifNotExists = { + ExtendedMetadata.EntityRequest.newBuilder().setEntityUri(uri) + .addQuery(query) + }) + } + } + } + + result += UnpackedMetadataResponse(alreadyCached.map { it.value.build() }) + + shouldRequest.map { it.value.build() }.chunked(400).forEach { + result += requestNetwork(it) + } - shouldRequest.map { it.value.build() }.chunked(400).forEach { - result += requestNetwork(it) + return@withContext result } - return@withContext result - } - - private fun requestNetwork( - queries: List - ): UnpackedMetadataResponse { - return try { - UnpackedMetadataResponse(spSessionManager.session.api().getExtendedMetadata( - ExtendedMetadata.BatchedEntityRequest.newBuilder().addAllEntityRequest(queries).build() - ).extendedMetadataList.also { saveUris(it) }) - } catch (e: Exception) { - e.printStackTrace() - UnpackedMetadataResponse(emptyList()) + private fun requestNetwork( + queries: List + ): UnpackedMetadataResponse { + return try { + UnpackedMetadataResponse(spSessionManager.session.api().getExtendedMetadata( + ExtendedMetadata.BatchedEntityRequest.newBuilder().addAllEntityRequest(queries) + .build() + ).extendedMetadataList.also { saveUris(it) }) + } catch (e: Exception) { + e.printStackTrace() + UnpackedMetadataResponse(emptyList()) + } } - } - - private fun saveUris(data: List) { - data.forEach { arr -> - val kind = arr.extensionKind - arr.extensionDataList.forEach { toSave -> - spMetadataDb.put(toSave.entityUri.let { uri -> - if (coreKinds.contains(kind)) uri else "$uri:${kind.ordinal}" - }, toSave.extensionData.value.toByteArray()) - } + + private fun saveUris(data: List) { + data.forEach { arr -> + val kind = arr.extensionKind + arr.extensionDataList.forEach { toSave -> + spMetadataDb.put(toSave.entityUri.let { uri -> + if (coreKinds.contains(kind)) uri else "$uri:${kind.ordinal}" + }, toSave.extensionData.value.toByteArray()) + } + } } - } } typealias MutableRequestEntities = MutableList>> typealias RequestEntities = List>> -fun MutableRequestEntities.user(uri: String) = add(uri to listOf(ExtensionKindOuterClass.ExtensionKind.USER_PROFILE)) -fun MutableRequestEntities.album(uri: String) = add(uri to listOf(ExtensionKindOuterClass.ExtensionKind.ALBUM_V4)) -fun MutableRequestEntities.artist(uri: String) = add(uri to listOf(ExtensionKindOuterClass.ExtensionKind.ARTIST_V4)) -fun MutableRequestEntities.track(uri: String) = add(uri to listOf(ExtensionKindOuterClass.ExtensionKind.TRACK_V4)) +fun MutableRequestEntities.user(uri: String) = + add(uri to listOf(ExtensionKindOuterClass.ExtensionKind.USER_PROFILE)) + +fun MutableRequestEntities.album(uri: String) = + add(uri to listOf(ExtensionKindOuterClass.ExtensionKind.ALBUM_V4)) + +fun MutableRequestEntities.artist(uri: String) = + add(uri to listOf(ExtensionKindOuterClass.ExtensionKind.ARTIST_V4)) + +fun MutableRequestEntities.track(uri: String) = + add(uri to listOf(ExtensionKindOuterClass.ExtensionKind.TRACK_V4)) + +fun MutableRequestEntities.tracks(uris: List) = + addAll(uris.map { it to listOf(ExtensionKindOuterClass.ExtensionKind.TRACK_V4) }) + +fun MutableRequestEntities.episodes(uris: List) = + addAll(uris.map { it to listOf(ExtensionKindOuterClass.ExtensionKind.EPISODE_V4) }) -fun MutableRequestEntities.tracks(uris: List) = addAll(uris.map { it to listOf(ExtensionKindOuterClass.ExtensionKind.TRACK_V4) }) -fun MutableRequestEntities.episodes(uris: List) = addAll(uris.map { it to listOf(ExtensionKindOuterClass.ExtensionKind.EPISODE_V4) }) -fun MutableRequestEntities.raw(uris: List) = addAll(uris.map { it to listOf(spotifyIdToKind(it)) }) +fun MutableRequestEntities.raw(uris: List) = + addAll(uris.map { it to listOf(spotifyIdToKind(it)) }) private fun spotifyIdToKind(id: String) = when (id.split(":")[1]) { - "track" -> ExtensionKindOuterClass.ExtensionKind.TRACK_V4 - "album" -> ExtensionKindOuterClass.ExtensionKind.ALBUM_V4 - "artist" -> ExtensionKindOuterClass.ExtensionKind.ARTIST_V4 - "episode" -> ExtensionKindOuterClass.ExtensionKind.EPISODE_V4 - else -> ExtensionKindOuterClass.ExtensionKind.UNKNOWN_EXTENSION + "track" -> ExtensionKindOuterClass.ExtensionKind.TRACK_V4 + "album" -> ExtensionKindOuterClass.ExtensionKind.ALBUM_V4 + "artist" -> ExtensionKindOuterClass.ExtensionKind.ARTIST_V4 + "episode" -> ExtensionKindOuterClass.ExtensionKind.EPISODE_V4 + else -> ExtensionKindOuterClass.ExtensionKind.UNKNOWN_EXTENSION } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpPlayerServiceImpl.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpPlayerServiceImpl.kt index 3be5da0e..2da02c72 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpPlayerServiceImpl.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpPlayerServiceImpl.kt @@ -9,6 +9,7 @@ import androidx.media2.common.SessionPlayer import androidx.media2.session.MediaController import androidx.media2.session.SessionCommandGroup import androidx.media2.session.SessionToken +import bruhcollective.itaysonlab.jetispot.core.lyrics.SpLyricsController import bruhcollective.itaysonlab.jetispot.core.util.Log import bruhcollective.itaysonlab.jetispot.playback.helpers.MediaItemWrapper import bruhcollective.itaysonlab.jetispot.playback.service.SpPlaybackService @@ -20,7 +21,7 @@ import kotlinx.coroutines.flow.flow class SpPlayerServiceImpl( private val context: Context, - private val manager: SpPlayerServiceManager + private val manager: SpPlayerServiceManager, ) : MediaController.ControllerCallback(), CoroutineScope by MainScope() { private var mediaController: MediaController? = null private var svcInit: (MediaController.() -> Unit)? = null @@ -32,8 +33,8 @@ class SpPlayerServiceImpl( if (manager.playbackState.value == SpPlayerServiceManager.PlaybackState.Playing) { emit(mediaController?.currentPosition ?: 0L) } - - delay(1000) + //Delay Ms + delay(50) } }.catch { emit(0L) @@ -138,7 +139,6 @@ class SpPlayerServiceImpl( p ) ) - manager.runExtra { it.onTrackProgressChanged(p) } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpPlayerServiceManager.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpPlayerServiceManager.kt index 0f927726..8f8ac7ce 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpPlayerServiceManager.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpPlayerServiceManager.kt @@ -2,12 +2,15 @@ package bruhcollective.itaysonlab.jetispot.core import android.content.Context import android.os.Bundle +import android.util.Log import androidx.annotation.FloatRange import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.core.util.Pair +import bruhcollective.itaysonlab.jetispot.core.lyrics.SpLyricsController +import bruhcollective.itaysonlab.jetispot.core.lyrics.SpLyricsRequester import bruhcollective.itaysonlab.jetispot.core.objs.player.PlayFromContextData import bruhcollective.itaysonlab.jetispot.core.objs.player.PlayFromContextPlayerData import bruhcollective.itaysonlab.jetispot.playback.helpers.MediaItemWrapper @@ -62,6 +65,7 @@ class SpPlayerServiceManager @Inject constructor( fun play(data: PlayFromContextData) = play(data.uri, data.player) fun play(uri: String, data: PlayFromContextPlayerData) = impl.awaitService { + Log.i("SpPlayerServiceManager", "Trying to play audio with $uri as URI and $data as data") setMediaUri(uri.toUri(), Bundle().also { it.putString("sp_json", moshi.adapter().toJson(data)) }) } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpSessionManager.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpSessionManager.kt index 562be965..3a7a5083 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpSessionManager.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpSessionManager.kt @@ -13,15 +13,23 @@ import javax.inject.Singleton @Suppress("BlockingMethodInNonBlockingContext") @Singleton class SpSessionManager @Inject constructor( - @ApplicationContext val appContext: Context, + @ApplicationContext val appContext: Context, ) { - private var _session: Session? = null - val session get() = _session ?: throw IllegalStateException("Session is not created yet!") + private var _session: Session? = null + val session get() = _session ?: throw IllegalStateException("Session is not created yet!") - fun createSession(): Session.Builder = Session.Builder(createCfg()).setDeviceType(Connect.DeviceType.SMARTPHONE).setDeviceName( - SpUtils.getDeviceName(appContext)).setDeviceId(null).setPreferredLocale(Locale.getDefault().language) - private fun createCfg() = Session.Configuration.Builder().setCacheEnabled(true).setDoCacheCleanUp(true).setCacheDir(File(appContext.cacheDir, "spa_cache")).setStoredCredentialsFile(File(appContext.filesDir, "spa_creds")).build() + fun createSession(): Session.Builder = + Session.Builder(createCfg()).setDeviceType(Connect.DeviceType.SMARTPHONE).setDeviceName( + SpUtils.getDeviceName(appContext) + ).setDeviceId(null).setPreferredLocale(Locale.getDefault().language) - fun isSignedIn() = _session?.isValid == true - fun setSession(s: Session) { _session = s } + private fun createCfg() = + Session.Configuration.Builder().setCacheEnabled(true).setDoCacheCleanUp(true) + .setCacheDir(File(appContext.cacheDir, "spa_cache")) + .setStoredCredentialsFile(File(appContext.filesDir, "spa_creds")).build() + + fun isSignedIn() = _session?.isValid == true + fun setSession(s: Session) { + _session = s + } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/api/SpInternalApi.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/api/SpInternalApi.kt index b57252eb..6ddde1d7 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/api/SpInternalApi.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/api/SpInternalApi.kt @@ -1,6 +1,7 @@ package bruhcollective.itaysonlab.jetispot.core.api import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubResponse +import bruhcollective.itaysonlab.jetispot.core.objs.playlists.LikedSongsResponse import bruhcollective.itaysonlab.jetispot.core.objs.tags.ContentFilterResponse import bruhcollective.itaysonlab.jetispot.core.util.SpUtils import bruhcollective.itaysonlab.jetispot.proto.SearchViewResponse @@ -23,35 +24,38 @@ interface SpInternalApi { suspend fun getChartView(): HubResponse @GET("/radio-apollo/v5/radio-hub") - suspend fun getRadioHub(): HubResponse + suspend fun getRadioHub(@Header("Accept-Language") language:String = Locale.getDefault().language): HubResponse + + @GET("/me/tracks") + suspend fun getSavedTracks(): LikedSongsResponse @GET("/hubview-mobile-v1/browse/{id}") - suspend fun getBrowseView(@Path("id") pageId: String = ""): HubResponse + suspend fun getBrowseView(@Path("id") pageId: String = "", @Header("Accept-Language") language:String = Locale.getDefault().language): HubResponse @GET("/album-entity-view/v2/album/{id}") - suspend fun getAlbumView(@Path("id") pageId: String, @Query("checkDeviceCapability") checkDeviceCapability: Boolean = true): HubResponse + suspend fun getAlbumView(@Path("id") pageId: String, @Query("checkDeviceCapability") checkDeviceCapability: Boolean = true, @Header("Accept-Language") language:String = Locale.getDefault().language): HubResponse @GET("/artistview/v1/artist/{id}") - suspend fun getArtistView(@Path("id") pageId: String, @Query("purchase_allowed") purchaseAllowed: Boolean = false, @Query("timeFormat") timeFormat: String = "24h"): HubResponse + suspend fun getArtistView(@Path("id") pageId: String, @Query("purchase_allowed") purchaseAllowed: Boolean = false, @Query("timeFormat") timeFormat: String = "24h", @Header("Accept-Language") language:String = Locale.getDefault().language): HubResponse @GET("/artistview/v1/artist/{id}/releases") - suspend fun getReleasesView(@Path("id") pageId: String, @Query("checkDeviceCapability") checkDeviceCapability: Boolean = true): HubResponse + suspend fun getReleasesView(@Path("id") pageId: String, @Query("checkDeviceCapability") checkDeviceCapability: Boolean = true, @Header("Accept-Language") language:String = Locale.getDefault().language): HubResponse @GET("/listening-history/v2/mobile/{timestamp}") - suspend fun getListeningHistory(@Path("timestamp") timestamp: String = "", @Query("type") type: String = "merged", @Query("last_component_had_play_context") idk: Boolean = false): HubResponse + suspend fun getListeningHistory(@Path("timestamp") timestamp: String = "", @Query("type") type: String = "merged", @Query("last_component_had_play_context") idk: Boolean = false, @Header("Accept-Language") language:String = Locale.getDefault().language): HubResponse @GET("/content-filter/v1/liked-songs") - @Headers("Accept: application/json", "Accept-Language: en-US") + @Headers("Accept: application/json") suspend fun getCollectionTags(@Query("subjective") subjective: Boolean = true): ContentFilterResponse @POST("/home-dac-viewservice/v1/view") - suspend fun getDacHome(@Body request: DacRequest = buildDacRequestForHome()): DacResponse + suspend fun getDacHome(@Body request: DacRequest = buildDacRequestForHome(), @Header("Accept-Language") acceptLanguage: String = Locale.getDefault().language): DacResponse @GET("/pam-view-service/v1/AllPlans") - suspend fun getAllPlans(): DacResponse + suspend fun getAllPlans(@Header("Accept-Language") language:String = Locale.getDefault().language): DacResponse @GET("/pam-view-service/v1/PlanOverview") - suspend fun getPlanOverview(): DacResponse + suspend fun getPlanOverview(@Header("Accept-Language") language:String = Locale.getDefault().language): DacResponse @GET("/popcount/v2/playlist/{id}/count") suspend fun getPlaylistPopCount(@Path("id") id: String = ""): Popcount2External.PopcountResult diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/SpCollectionManager.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/SpCollectionManager.kt index 73022ddf..fc5720d7 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/SpCollectionManager.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/SpCollectionManager.kt @@ -25,6 +25,7 @@ class SpCollectionManager @Inject constructor( private val dao: LocalCollectionDao, private val metadataRequester: SpMetadataRequester ): DealerClient.MessageListener { + // it's important to use queuing here private val scopeDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val scope = CoroutineScope(scopeDispatcher + SupervisorJob()) @@ -64,7 +65,7 @@ class SpCollectionManager @Inject constructor( } suspend fun performScanIfEmpty(of: String) { - Log.d("SpColManager", "Performing scan of $of (if empty)") + Log.d("SpCollectionManager", "Performing scan of $of (if empty)") dao.getCollection(of) ?: writer.performScan(of) } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/di/ApiModule.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/di/ApiModule.kt index 48e29f0b..710c57de 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/di/ApiModule.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/di/ApiModule.kt @@ -1,8 +1,10 @@ package bruhcollective.itaysonlab.jetispot.core.di +import bruhcollective.itaysonlab.jetispot.BuildConfig import bruhcollective.itaysonlab.jetispot.core.SpSessionManager import bruhcollective.itaysonlab.jetispot.core.api.* import bruhcollective.itaysonlab.jetispot.core.di.ext.interceptRequest +import bruhcollective.itaysonlab.jetispot.core.util.Log import bruhcollective.itaysonlab.jetispot.core.util.SpUtils import bruhcollective.itaysonlab.jetispot.core.util.create import com.squareup.moshi.Moshi @@ -30,7 +32,13 @@ object ApiModule { interceptRequest { orig -> // 1. Authorization (& client token) header("Authorization", "Bearer ${sessionManager.session.tokens().get("playlist-read")}") + if(BuildConfig.DEBUG){ + Log.d("Authorization Bearer token", "Bearer ${sessionManager.session.tokens().get("playlist-read")}") + } header("client-token", tokenHandler.requestToken()) + if(BuildConfig.DEBUG){ + Log.d("Authorization Client Token", tokenHandler.requestToken()) + } // 2. Default headers header("User-Agent", "Spotify/${SpUtils.SPOTIFY_APP_VERSION} Android/32 (Pixel 4a (5G))") diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/lyrics/SpLyricsController.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/lyrics/SpLyricsController.kt index 682a5e4a..b0c749ef 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/lyrics/SpLyricsController.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/lyrics/SpLyricsController.kt @@ -1,20 +1,23 @@ package bruhcollective.itaysonlab.jetispot.core.lyrics +import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import bruhcollective.itaysonlab.jetispot.R +import bruhcollective.itaysonlab.jetispot.SpApp +import bruhcollective.itaysonlab.jetispot.core.SpPlayerServiceManager import com.spotify.lyrics.v2.lyrics.proto.LyricsResponse.LyricsLine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import xyz.gianlu.librespot.common.Base62 -import xyz.gianlu.librespot.common.Utils -import xyz.gianlu.librespot.metadata.TrackId import javax.inject.Inject class SpLyricsController @Inject constructor( - private val requester: SpLyricsRequester + private val requester: SpLyricsRequester, + private val manager: SpPlayerServiceManager ): CoroutineScope by MainScope() { private val base62 = Base62.createInstanceWithInvertedCharacterSet() private var _songJob: Job? = null @@ -52,10 +55,19 @@ class SpLyricsController @Inject constructor( } } - fun setProgress(pos: Long) { - currentSongLine = currentLyricsLines.firstOrNull { - it.startTimeMs >= pos - }?.words ?: "" + @Suppress("SENSELESS_COMPARISON", "LiftReturnOrAssignment") + fun setProgress(pos: Long){ + val lyricIndex = currentLyricsLines.indexOfLast { it.startTimeMs < pos } + + if(currentLyricsState == LyricsState.Unavailable){ + currentSongLine = SpApp.context.getString(R.string.no_lyrics) + } else { + if (lyricIndex == -1) { + currentSongLine = "\uD83C\uDFB5" + } else { + currentSongLine = currentLyricsLines[lyricIndex].words + } + } } class ProviderInfo( diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/hub/HubComponent.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/hub/HubComponent.kt index 5181fab1..58ea8c8c 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/hub/HubComponent.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/hub/HubComponent.kt @@ -26,8 +26,13 @@ sealed class HubComponent { @TypeLabel("artist:likedSongsRow") object ArtistLikedSongs: HubComponent() - // BROWSE + @TypeLabel("listeninghistory:playlistContextRow", alternateLabels = ["listeninghistory:collectionContextRow", "listeninghistory:albumContextRow"]) + object HistoryPlaylist: HubComponent() + + @TypeLabel("listeninghistory:dividerAfterPlaysFromContextRow") + object HistoryDivider: HubComponent() + // BROWSE @TypeLabel("find:categoryCard") object FindCard: HubComponent(), ComponentInGrid @@ -96,7 +101,7 @@ sealed class HubComponent { // IGNORING - @TypeLabel("listeninghistory:dividerAfterEntityRow") + @TypeLabel("listeninghistory:dividerAfterEntityRow", alternateLabels = ["listeninghistory:playsFromContextRow", "listeninghistory:artistContextRow" /*This by the moment*/]) object EmptySpace: HubComponent() @TypeLabel("freetier:offlineSwitchComponent", alternateLabels = ["find:header"]) diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/playlists/LikedSongsResponse.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/playlists/LikedSongsResponse.kt new file mode 100644 index 00000000..e21f6961 --- /dev/null +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/playlists/LikedSongsResponse.kt @@ -0,0 +1,15 @@ +package bruhcollective.itaysonlab.jetispot.core.objs.playlists + +import bruhcollective.itaysonlab.jetispot.ui.hub.virt.PlaylistEntityView +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +class LikedSongsResponse( + val href: String? = "", + val items: List, + val limit: Int? = 0, + val next: String? = "", + val offset: Int? = 0, + val previous: String? = "", + val total: Int? = 0, +) \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/UpdateUtil.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/UpdateUtil.kt new file mode 100644 index 00000000..fc121ac6 --- /dev/null +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/UpdateUtil.kt @@ -0,0 +1,258 @@ +package bruhcollective.itaysonlab.jetispot.core.util + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import androidx.core.content.FileProvider +import bruhcollective.itaysonlab.jetispot.SpApp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody +import okio.IOException +import java.io.File +import java.util.regex.Pattern +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +object UpdateUtil { + + private const val OWNER = "iTaysonLab" + private const val REPO = "jetispot" + private const val ARM64 = "arm64-v8a" + private const val ARM32 = "armeabi-v7a" + private const val X86 = "x86" + private const val X64 = "x86_64" + private const val TAG = "UpdateUtil" + + private val client = OkHttpClient() + private val requestForLatestRelease = + Request.Builder().url("https://api.github.com/repos/${OWNER}/${REPO}/releases/latest") + .build() + private val jsonFormat = Json { ignoreUnknownKeys = true } + + private suspend fun getLatestRelease(): LatestRelease { + return suspendCoroutine { continuation -> + client.newCall(requestForLatestRelease).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val responseData = response.body!!.string() + val latestRelease = jsonFormat.decodeFromString(responseData) + response.body!!.close() + continuation.resume(latestRelease) + } + + override fun onFailure(call: Call, e: IOException) { + continuation.resumeWithException(e) + } + }) + } + } + + suspend fun checkForUpdate(context: Context = SpApp.context): LatestRelease? { + val currentVersion = context.getCurrentVersion() + val latestRelease = getLatestRelease() + val latestVersion = Version(latestRelease.name ?: "") + return if (currentVersion < latestVersion) latestRelease + else null + } + + private fun Context.getCurrentVersion() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getPackageInfo( + packageName, PackageManager.PackageInfoFlags.of(0) + ).versionName.toVersion() + } else { + packageManager.getPackageInfo( + packageName, 0 + ).versionName.toVersion() + } + + private fun String.toVersion() = Version(this) + + private fun Context.getLatestApk() = + File(getExternalFilesDir("apk"), "latest.apk") + + private fun Context.getFileProvider() = "${packageName}.provider" + + fun installLatestApk(context: Context = SpApp.context) = context.apply { + kotlin.runCatching { + val contentUri = FileProvider.getUriForFile(this, getFileProvider(), getLatestApk()) + val intent = Intent(Intent.ACTION_VIEW).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + setDataAndType(contentUri, "application/vnd.android.package-archive") + } + startActivity(intent) + }.onFailure { throwable: Throwable -> + throwable.printStackTrace() + Log.e(TAG, "installLatestApk: ", throwable) + } + } + + suspend fun downloadApk( + context: Context = SpApp.context, + latestRelease: LatestRelease + ): Flow = withContext(Dispatchers.IO) { + val apkVersion = context.packageManager.getPackageArchiveInfo( + context.getLatestApk().absolutePath, 0 + )?.versionName?.toVersion() ?: Version() + + Log.d(TAG, apkVersion.toString()) + + if (apkVersion >= Version(latestRelease.tagName.toString()) + ) { + return@withContext flow { emit(DownloadStatus.Finished(context.getLatestApk())) } + } + + val isArmArchSupported = Build.SUPPORTED_ABIS.contains(ARM32) + val is64BitsArchSupported = with(Build.SUPPORTED_ABIS) { + if (isArmArchSupported) contains(ARM64) + else contains(X64) + } + val preferredArch = if (is64BitsArchSupported) { + if (isArmArchSupported) ARM64 else X64 + } else { + if (isArmArchSupported) ARM32 else X86 + } + + val targetUrl = latestRelease.assets?.find { + return@find it.name?.contains(preferredArch) ?: false + }?.browserDownloadUrl ?: return@withContext emptyFlow() + val request = Request.Builder().url(targetUrl).build() + try { + val response = client.newCall(request).execute() + val responseBody = response.body + return@withContext responseBody!!.downloadFileWithProgress(context.getLatestApk()) + } catch (e: Exception) { + e.printStackTrace() + } + emptyFlow() + } + + + private fun ResponseBody.downloadFileWithProgress(saveFile: File): Flow = flow { + emit(DownloadStatus.Progress(0)) + + var deleteFile = true + + try { + byteStream().use { inputStream -> + saveFile.outputStream().use { outputStream -> + val totalBytes = contentLength() + val data = ByteArray(8_192) + var progressBytes = 0L + + while (true) { + val bytes = inputStream.read(data) + + if (bytes == -1) { + break + } + + outputStream.channel + outputStream.write(data, 0, bytes) + progressBytes += bytes + emit(DownloadStatus.Progress(percent = ((progressBytes * 100) / totalBytes).toInt())) + } + + when { + progressBytes < totalBytes -> throw Exception("missing bytes") + progressBytes > totalBytes -> throw Exception("too many bytes") + else -> deleteFile = false + } + } + } + + emit(DownloadStatus.Finished(saveFile)) + } finally { + if (deleteFile) { + saveFile.delete() + } + } + }.flowOn(Dispatchers.IO).distinctUntilChanged() + + @Serializable + data class LatestRelease( + @SerialName("html_url") val htmlUrl: String? = null, + @SerialName("tag_name") val tagName: String? = null, + val name: String? = null, + val draft: Boolean? = null, + @SerialName("prerelease") val preRelease: Boolean? = null, + @SerialName("created_at") val createdAt: String? = null, + @SerialName("published_at") val publishedAt: String? = null, + val assets: List? = null, + val body: String? = null, + ) + + @Serializable + data class AssetsItem( + val name: String? = null, + @SerialName("content_type") val contentType: String? = null, + val size: Int? = null, + @SerialName("download_count") val downloadCount: Int? = null, + @SerialName("created_at") val createdAt: String? = null, + @SerialName("updated_at") val updatedAt: String? = null, + @SerialName("browser_download_url") val browserDownloadUrl: String? = null, + ) + + sealed class DownloadStatus { + object NotYet : DownloadStatus() + data class Progress(val percent: Int) : DownloadStatus() + data class Finished(val file: File) : DownloadStatus() + } + + class Version( + versionName: String = "v0.0.0", + ) { + var major: Int = 0 + private set + var minor: Int = 0 + private set + var patch: Int = 0 + private set + var build: Int = 0 + private set + + private fun toNumber(): Long { + return major * MAJOR + minor * MINOR + patch * PATCH + build * BUILD + } + + companion object { + private val pattern = Pattern.compile("""v?(\d+)\.(\d+)\.(\d+)(-.*?(\d+))*""") + + private const val BUILD = 1L + private const val PATCH = 100L + private const val MINOR = 10_000L + private const val MAJOR = 1_000_000L + } + + init { + val matcher = pattern.matcher(versionName) + if (matcher.find()) { + major = matcher.group(1)?.toInt() ?: 0 + minor = matcher.group(2)?.toInt() ?: 0 + patch = matcher.group(3)?.toInt() ?: 0 + build = matcher.group(5)?.toInt() ?: 99 + // Prioritize stable versions + } + } + + operator fun compareTo(other: Version) = toNumber().compareTo(other.toNumber()) + } +} \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/app_preferences/AppPreferencesUtil.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/app_preferences/AppPreferencesUtil.kt new file mode 100644 index 00000000..b992a190 --- /dev/null +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/app_preferences/AppPreferencesUtil.kt @@ -0,0 +1,38 @@ +package bruhcollective.itaysonlab.jetispot.core.util.app_preferences + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit + +object AppPreferencesUtil { + private var sharedPreferences: SharedPreferences? = null + + fun setupConfig(context: Context) { + sharedPreferences = context.getSharedPreferences("jetispot.appconfig", + Context.MODE_PRIVATE + ) + } + + /* var Setting1: Float? + get() = Key./*Get the key*/./*function*/ + set(value) = /*Same*/ +*/ + + private enum class Key { + Setting1; + + fun getBoolean(defValue: Boolean = false, elseValue: Boolean? = null): Boolean? = if (sharedPreferences?.contains(name) == true) sharedPreferences!!.getBoolean(name, defValue) else elseValue + fun getFloat(defValue: Float = 0f, elseValue: Float? = null): Float? = if (sharedPreferences?.contains(name) == true) sharedPreferences!!.getFloat(name, defValue) else elseValue + fun getInt(defValue: Int = 0, elseValue: Int? = null): Int? = if (sharedPreferences?.contains(name) == true) sharedPreferences!!.getInt(name, defValue) else elseValue + fun getLong(defValue: Long = 0, elseValue: Long? = null): Long? = if (sharedPreferences?.contains(name) == true) sharedPreferences!!.getLong(name, defValue) else elseValue + fun getString(defValue: String = "", elseValue: String? = null): String? = if (sharedPreferences?.contains(name) == true) sharedPreferences!!.getString(name, defValue) else elseValue + + fun setBoolean(value: Boolean?) = value?.let { sharedPreferences!!.edit { putBoolean(name, value) } } ?: remove() + fun setFloat(value: Float?) = value?.let { sharedPreferences!!.edit { putFloat(name, value) } } ?: remove() + fun setInt(value: Int?) = value?.let { sharedPreferences!!.edit { putInt(name, value) } } ?: remove() + fun setLong(value: Long?) = value?.let { sharedPreferences!!.edit { putLong(name, value) } } ?: remove() + fun setString(value: String?) = value?.let { sharedPreferences!!.edit { putString(name, value) } } ?: remove() + + fun remove() = sharedPreferences!!.edit { remove(name) } + } +} \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/helpers/MoreOptionsDialogHelper.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/helpers/MoreOptionsDialogHelper.kt new file mode 100644 index 00000000..f7cd36bd --- /dev/null +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/helpers/MoreOptionsDialogHelper.kt @@ -0,0 +1,9 @@ +package bruhcollective.itaysonlab.jetispot.core.util.helpers + +import bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying.NowPlayingViewModel + +class MoreOptionsDialogHelper( + private val nowPlayingViewModel: NowPlayingViewModel +) { + +} \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/playback/helpers/MediaItemWrapper.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/playback/helpers/MediaItemWrapper.kt index 39e37986..aedb7254 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/playback/helpers/MediaItemWrapper.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/playback/helpers/MediaItemWrapper.kt @@ -3,6 +3,7 @@ package bruhcollective.itaysonlab.jetispot.playback.helpers import androidx.compose.ui.graphics.asImageBitmap import androidx.media2.common.MediaItem import androidx.media2.common.MediaMetadata +import bruhcollective.itaysonlab.jetispot.R class MediaItemWrapper( private val item: MediaItem? = null diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/AppNavigation.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/AppNavigation.kt index d061e359..d7c3ee7c 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/AppNavigation.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/AppNavigation.kt @@ -1,13 +1,26 @@ package bruhcollective.itaysonlab.jetispot.ui +import android.Manifest import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.NewReleases +import androidx.compose.material.icons.rounded.Download import androidx.compose.material.icons.rounded.Warning import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -15,9 +28,12 @@ import androidx.compose.ui.unit.dp import androidx.navigation.* import androidx.navigation.compose.* import bruhcollective.itaysonlab.jetispot.R +import bruhcollective.itaysonlab.jetispot.SpApp.Companion.context import bruhcollective.itaysonlab.jetispot.core.SpAuthManager import bruhcollective.itaysonlab.jetispot.core.SpSessionManager import bruhcollective.itaysonlab.jetispot.core.api.SpInternalApi +import bruhcollective.itaysonlab.jetispot.core.util.UpdateUtil +import bruhcollective.itaysonlab.jetispot.ui.bottomsheets.MoreOptionsBottomSheet import bruhcollective.itaysonlab.jetispot.ui.bottomsheets.jump_to_artist.JumpToArtistBottomSheet import bruhcollective.itaysonlab.jetispot.ui.screens.BottomSheet import bruhcollective.itaysonlab.jetispot.ui.screens.Dialog @@ -33,6 +49,9 @@ import bruhcollective.itaysonlab.jetispot.ui.screens.search.SearchScreen import bruhcollective.itaysonlab.jetispot.ui.screens.yourlibrary2.YourLibraryContainerScreen import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi import com.google.accompanist.navigation.material.bottomSheet +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialNavigationApi::class) @Composable @@ -42,6 +61,36 @@ fun AppNavigation( authManager: SpAuthManager, modifier: Modifier ) { + + var showUpdateDialog by rememberSaveable { mutableStateOf(false) } + var currentDownloadStatus by remember { mutableStateOf(UpdateUtil.DownloadStatus.NotYet as UpdateUtil.DownloadStatus) } + val scope = rememberCoroutineScope() + var updateJob: Job? = null + var latestRelease by remember { mutableStateOf(UpdateUtil.LatestRelease()) } + val settings = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + UpdateUtil.installLatestApk() + } + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { result -> + if (result) { + UpdateUtil.installLatestApk() + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (!context.packageManager.canRequestPackageInstalls()) + settings.launch( + Intent( + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, + Uri.parse("package:${context.packageName}"), + ) + ) + else + UpdateUtil.installLatestApk() + } + } + } + LaunchedEffect(Unit) { if (sessionManager.isSignedIn()) return@LaunchedEffect authManager.authStored() @@ -49,6 +98,19 @@ fun AppNavigation( popUpTo(Screen.NavGraph.route) } } + LaunchedEffect(Unit){ + launch(Dispatchers.IO) { + kotlin.runCatching { + val temp = UpdateUtil.checkForUpdate() + if (temp != null) { + latestRelease = temp + showUpdateDialog = true + } + }.onFailure { + it.printStackTrace() + } + } + } NavHost( navController = navController, @@ -104,6 +166,8 @@ fun AppNavigation( composable(Screen.Search.route) { SearchScreen() } composable(Screen.Library.route) { YourLibraryContainerScreen() } + //DIALOGS + dialog(Dialog.AuthDisclaimer.route) { AlertDialog(onDismissRequest = { navController.popBackStack() }, icon = { Icon(Icons.Rounded.Warning, null) @@ -140,9 +204,87 @@ fun AppNavigation( }) } + //Bottom Sheet Dialogs + bottomSheet(BottomSheet.JumpToArtist.route) { entry -> val data = remember { entry.arguments!!.getString("artistIdsAndRoles")!! } JumpToArtistBottomSheet(data = data) } + + bottomSheet(BottomSheet.MoreOptions.route) { entry -> + val trackName = remember {entry.arguments!!.getString("trackName")!!} + val artistName = remember {entry.arguments!!.getString("artistName")!!} + val artworkUrl = remember {entry.arguments!!.getString("artworkUrl")!!} + val artistsData = remember {entry.arguments!!.getString("artistsData")!!} + MoreOptionsBottomSheet(trackName, artistName, artworkUrl, artistsData) + } + } + + if (showUpdateDialog) { + UpdateDialog( + onDismissRequest = { + showUpdateDialog = false + updateJob?.cancel() + }, + title = latestRelease.name.toString(), + onConfirmUpdate = { + updateJob = scope.launch(Dispatchers.IO) { + kotlin.runCatching { + UpdateUtil.downloadApk(latestRelease = latestRelease) + .collect { downloadStatus -> + currentDownloadStatus = downloadStatus + if (downloadStatus is UpdateUtil.DownloadStatus.Finished) { + launcher.launch(Manifest.permission.REQUEST_INSTALL_PACKAGES) + } + } + }.onFailure { + it.printStackTrace() + currentDownloadStatus = UpdateUtil.DownloadStatus.NotYet + //TODO: Show error + return@launch + } + } + }, + releaseNote = latestRelease.body.toString(), + downloadStatus = currentDownloadStatus + ) + } +} + + +@Composable +fun UpdateDialog( + onDismissRequest: () -> Unit, + title: String, + onConfirmUpdate: () -> Unit, + releaseNote: String, + downloadStatus: UpdateUtil.DownloadStatus, +) { + AlertDialog( + onDismissRequest = {}, + title = { + Text(title) + + }, + icon = { Icon(Icons.Outlined.NewReleases, null) }, confirmButton = { + TextButton(onClick = { if (downloadStatus !is UpdateUtil.DownloadStatus.Progress) onConfirmUpdate() }) { + when (downloadStatus) { + is UpdateUtil.DownloadStatus.Progress -> Text("${downloadStatus.percent} %") + else -> Text(stringResource(R.string.update)) + } + } + }, dismissButton = { + DismissButton { onDismissRequest() } + }, text = { + Column(Modifier.verticalScroll(rememberScrollState())) { + Text(releaseNote) + } + }) +} + +@Composable +fun DismissButton(text: String = stringResource(R.string.dismiss), onClick: () -> Unit) { + TextButton(onClick = onClick) { + Text(text) } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/blocks/TwoColumnAndImageBlock.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/blocks/TwoColumnAndImageBlock.kt index 0c3da514..e7eb9a6d 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/blocks/TwoColumnAndImageBlock.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/blocks/TwoColumnAndImageBlock.kt @@ -15,18 +15,43 @@ import xyz.gianlu.librespot.metadata.ImageId @Composable fun TwoColumnAndImageBlock( - artworkUri: String?, + artworkUri: String? = "spotify:image:ab67706c0000bebb8d0ce13d55f634e290f744ba", title: String, text: String, modifier: Modifier = Modifier ) { - Row(modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)) { - PreviewableAsyncImage(imageUrl = remember(artworkUri) { - artworkUri?.let { "https://i.scdn.co/image/" + ImageId.fromUri(it).hexId() } - }, placeholderType = "track", modifier = Modifier - .size(48.dp)) - Column(Modifier.padding(horizontal = 12.dp).align(Alignment.CenterVertically)) { + Row( + modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + ) { + if (artworkUri!!.startsWith("spotify:image")) { + val imageId = remember { artworkUri } + PreviewableAsyncImage( + imageUrl = remember(artworkUri) { + artworkUri.let { + "https://i.scdn.co/image/" + ImageId.fromUri( + imageId + ).hexId() + } + }, placeholderType = "track", modifier = Modifier + .size(48.dp) + ) + } else { + PreviewableAsyncImage( + imageUrl = artworkUri, + modifier = Modifier + .size(48.dp), + placeholderType = "track" + ) + } + + Column( + Modifier + .padding(horizontal = 12.dp) + .align(Alignment.CenterVertically) + ) { MediumText(title, fontWeight = FontWeight.Normal, fontSize = 18.sp) Spacer(Modifier.height(4.dp)) Subtext(text, modifier = Modifier, maxLines = 1) diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/bottomsheets/MoreOptionsBottomSheet.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/bottomsheets/MoreOptionsBottomSheet.kt new file mode 100644 index 00000000..e7eec57c --- /dev/null +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/bottomsheets/MoreOptionsBottomSheet.kt @@ -0,0 +1,176 @@ +package bruhcollective.itaysonlab.jetispot.ui.bottomsheets + +import android.graphics.drawable.Icon +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.rounded.Person +import androidx.compose.material.icons.rounded.Share +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import bruhcollective.itaysonlab.jetispot.R +import bruhcollective.itaysonlab.jetispot.core.util.SpUtils +import bruhcollective.itaysonlab.jetispot.core.util.helpers.MoreOptionsDialogHelper +import bruhcollective.itaysonlab.jetispot.ui.ext.compositeSurfaceElevation +import bruhcollective.itaysonlab.jetispot.ui.navigation.LocalNavigationController +import bruhcollective.itaysonlab.jetispot.ui.screens.BottomSheet +import bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying.NowPlayingViewModel +import bruhcollective.itaysonlab.jetispot.ui.shared.MarqueeText +import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage +import com.spotify.metadata.Metadata +import xyz.gianlu.librespot.common.Utils +import xyz.gianlu.librespot.metadata.ArtistId + +@Composable +fun MoreOptionsBottomSheet( + trackName: String, + artistName: String, + artworkUrl: String, + artistsData: String, +) { + val navController = LocalNavigationController.current + + val decodedUrl = "https://i.scdn.co/image/${artworkUrl}" + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.compositeSurfaceElevation(8.dp)) + .navigationBarsPadding() + ) { + Divider( + modifier = Modifier + .width(32.dp) + .padding(vertical = 14.dp) + .clip(CircleShape) + .align(Alignment.CenterHorizontally), + thickness = 4.dp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.4f) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(id = R.string.more_options), + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) + + Row( + modifier = Modifier.padding(top = 8.dp, start = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + ElevatedCard(modifier = Modifier.clip(RoundedCornerShape(8.dp)), elevation = CardDefaults.cardElevation(8.dp)) { + PreviewableAsyncImage( + imageUrl = decodedUrl, + placeholderType = "track", + modifier = Modifier.size(64.dp) + ) + } + Column(modifier = Modifier.padding(start = 16.dp, end = 8.dp)) { + MarqueeText( + text = trackName, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(4.dp)) + MarqueeText( + text = artistName, + color = MaterialTheme.colorScheme.onSurface.copy(0.7f), + fontSize = 12.sp, + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + Divider( + modifier = Modifier + .clip(CircleShape) + .align(Alignment.CenterHorizontally) + .padding(horizontal = 12.dp), + thickness = 3.dp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.4f) + ) + Column(modifier = Modifier) { + actionButton( + icon = Icons.Rounded.Person, + text = stringResource(id = R.string.go_to_artist), + onClick = { + navController.navigate(BottomSheet.JumpToArtist, mapOf( + "artistIdsAndRoles" to artistsData + ))}) + buttonsDivider() + actionButton( + icon = Icons.Filled.Star, + text = stringResource(id = R.string.save_to_your_music), + onClick = {}) + buttonsDivider() + actionButton( + icon = Icons.Rounded.Share, + text = stringResource(id = R.string.share), + onClick = {}) + } + } +} + +@Composable +private fun actionButton( + icon: ImageVector, + text: String, + onClick: () -> Unit +) { + Box(modifier = Modifier.padding()) { + Row( + modifier = Modifier + .clickable(onClick = onClick) + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .size(24.dp) + ) + Text( + text = text, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 14.sp, + modifier = Modifier.padding(start = 16.dp) + ) + } + } +} + +@Composable +private fun buttonsDivider() { + Divider( + modifier = Modifier.padding(horizontal = 16.dp), + thickness = 1.dp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.4f) + ) +} + +@Preview +@Composable +fun actionButtonPreview() { + actionButton(icon = Icons.Default.Star, text = "Go to song artist", onClick = {}) +} \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/bottomsheets/jump_to_artist/JumpToArtistBottomSheet.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/bottomsheets/jump_to_artist/JumpToArtistBottomSheet.kt index 9cbbf338..b42b207d 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/bottomsheets/jump_to_artist/JumpToArtistBottomSheet.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/bottomsheets/jump_to_artist/JumpToArtistBottomSheet.kt @@ -5,14 +5,20 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Divider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import bruhcollective.itaysonlab.jetispot.R @@ -25,54 +31,83 @@ import xyz.gianlu.librespot.metadata.ArtistId @Composable fun JumpToArtistBottomSheet( - data: String + data: String ) { - val navController = LocalNavigationController.current + val navController = LocalNavigationController.current - val content = remember { - data.split("|").map { - Metadata.ArtistWithRole.parseFrom(Utils.hexToBytes(it)) - }.map { - Triple( - ArtistId.fromHex(Utils.bytesToHex(it.artistGid)).toSpotifyUri(), - it.artistName, - when (it.role) { - Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_MAIN_ARTIST -> R.string.artist_role_main - Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_FEATURED_ARTIST -> R.string.artist_role_feat - Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_REMIXER -> R.string.artist_role_remixer - Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_ACTOR -> R.string.artist_role_actor - Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_COMPOSER -> R.string.artist_role_composer - Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_CONDUCTOR -> R.string.artist_role_conductor - Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_ORCHESTRA -> R.string.artist_role_orchestra - else -> R.string.artist_role_unknown + val content = remember { + data.split("|").map { + Metadata.ArtistWithRole.parseFrom(Utils.hexToBytes(it)) + }.map { + Triple( + ArtistId.fromHex(Utils.bytesToHex(it.artistGid)).toSpotifyUri(), + it.artistName, + when (it.role) { + Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_MAIN_ARTIST -> R.string.artist_role_main + Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_FEATURED_ARTIST -> R.string.artist_role_feat + Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_REMIXER -> R.string.artist_role_remixer + Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_ACTOR -> R.string.artist_role_actor + Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_COMPOSER -> R.string.artist_role_composer + Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_CONDUCTOR -> R.string.artist_role_conductor + Metadata.ArtistWithRole.ArtistRole.ARTIST_ROLE_ORCHESTRA -> R.string.artist_role_orchestra + else -> R.string.artist_role_unknown + } + ) } - ) } - } - Column( - Modifier - .background(MaterialTheme.colorScheme.compositeSurfaceElevation(8.dp)) - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .navigationBarsPadding()) { - MediumText(text = "Choose an artist", fontSize = 21.sp, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp, bottom = 8.dp)) + Column( + Modifier + .background(MaterialTheme.colorScheme.compositeSurfaceElevation(8.dp)) + .fillMaxWidth() + .navigationBarsPadding() + ) { + Divider( + modifier = Modifier + .width(32.dp) + .padding(vertical = 14.dp) + .clip(CircleShape) + .align(Alignment.CenterHorizontally), + thickness = 4.dp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.4f) + ) + + Text( + text = stringResource(id = R.string.choose_artist), + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) - LazyColumn { - items(content) { artist -> - Column( - Modifier - .clickable { - navController.popBackStack() - navController.navigate(artist.first) - } - .padding(horizontal = 16.dp, vertical = 8.dp) - .fillMaxWidth()) { - MediumText(text = artist.second, color = MaterialTheme.colorScheme.onSurface) - Spacer(modifier = Modifier.height(2.dp)) - Text(text = stringResource(id = artist.third), maxLines = 1, color = MaterialTheme.colorScheme.onSurface) + LazyColumn { + items(content) { artist -> + Column( + Modifier + .clickable { + navController.popBackStack() + navController.navigate(artist.first) + } + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth() + ) { + Text( + text = artist.second, + fontSize = 18.sp, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = stringResource(id = artist.third), + fontSize = 16.sp, + maxLines = 1, + color = MaterialTheme.colorScheme.onSurface.copy(0.7f) + ) + } } } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/DacRender.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/DacRender.kt index fa521da8..0686af02 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/DacRender.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/DacRender.kt @@ -26,6 +26,7 @@ fun DacRender ( item: Message ) { when (item) { + // AllPlans / PlanOverview is MultiUserMemberComponent -> MultiUserMemberComponentBinder(item) is BenefitListComponent -> BenefitListComponentBinder(item) @@ -34,6 +35,8 @@ fun DacRender ( is SingleUserRecurringComponent -> SingleUserComponentBinder(item) is SingleUserPrepaidComponent -> SingleUserComponentBinder(item) is SingleUserTrialComponent -> SingleUserComponentBinder(item) + is FallbackPlanComponent -> FallbackPlanComponentBinder(item) + // Home is ToolbarComponent -> ToolbarComponentBinder(item) is ToolbarComponentV2 -> ToolbarComponent2Binder(item) @@ -53,6 +56,9 @@ fun DacRender ( is SectionComponent -> SectionComponentBinder(item) is RecentlyPlayedSectionComponent -> RecentlyPlayedSectionComponentBinder() + //Podcasts + //EpisodeCardActionsMediumComponent -> + // is SnappyGridSectionComponent -> SnappyGridSectionComponentBinder(item) // Other diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_home/ShortcutsBinder.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_home/ShortcutsBinder.kt index 5e6eb6b4..20411f25 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_home/ShortcutsBinder.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_home/ShortcutsBinder.kt @@ -16,40 +16,96 @@ import com.spotify.home.dac.component.v1.proto.* @Composable fun ShortcutsBinder( - item: ShortcutsSectionComponent + item: ShortcutsSectionComponent ) { - item.shortcutsList.map { it.dynamicUnpack() }.chunked(2).forEachIndexed { idx, pairs -> - Row(Modifier.padding(horizontal = 16.dp).padding(bottom = if (idx != item.shortcutsList.lastIndex / 2) 8.dp else 0.dp)) { - pairs.forEachIndexed { xIdx, xItem -> - Box(Modifier.weight(1f).padding(end = if (xIdx == 0) 8.dp else 0.dp)) { - when (xItem) { - is AlbumCardShortcutComponent -> ShortcutComponentBinder(xItem.navigateUri, xItem.imageUri, "album", xItem.title) - is PlaylistCardShortcutComponent -> ShortcutComponentBinder(xItem.navigateUri, xItem.imageUri, "playlist", xItem.title) - is ShowCardShortcutComponent -> ShortcutComponentBinder(xItem.navigateUri, xItem.imageUri, "podcasts", xItem.title) - is ArtistCardShortcutComponent -> ShortcutComponentBinder(xItem.navigateUri, xItem.imageUri, "artist", xItem.title) - is EpisodeCardShortcutComponent -> ShortcutComponentBinder(xItem.navigateUri, xItem.imageUri, "podcasts", xItem.title) - } + item.shortcutsList.map { it.dynamicUnpack() }.chunked(2).forEachIndexed { idx, pairs -> + Row( + Modifier + .padding(horizontal = 16.dp) + .padding(bottom = if (idx != item.shortcutsList.lastIndex / 2) 8.dp else 0.dp) + ) { + pairs.forEachIndexed { xIdx, xItem -> + Box( + Modifier + .weight(1f) + .padding(end = if (xIdx == 0) 8.dp else 0.dp)) { + when (xItem) { + is AlbumCardShortcutComponent -> ShortcutComponentBinder( + xItem.navigateUri, + xItem.imageUri, + "album", + xItem.title + ) + is PlaylistCardShortcutComponent -> ShortcutComponentBinder( + xItem.navigateUri, + xItem.imageUri, + "playlist", + xItem.title + ) + is ShowCardShortcutComponent -> ShortcutComponentBinder( + xItem.navigateUri, + xItem.imageUri, + "podcasts", + xItem.title + ) + is ArtistCardShortcutComponent -> ShortcutComponentBinder( + xItem.navigateUri, + xItem.imageUri, + "artist", + xItem.title + ) + is EpisodeCardShortcutComponent -> ShortcutComponentBinder( + xItem.navigateUri, + xItem.imageUri, + "podcasts", + xItem.title + ) + } + } + } } - } } - } } @Composable private fun ShortcutComponentBinder( - navigateUri: String, - imageUrl: String, - imagePlaceholder: String, - title: String + navigateUri: String, + imageUrl: String, + imagePlaceholder: String, + title: String ) { - Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.compositeSurfaceElevation(3.dp)), modifier = Modifier.height(56.dp).fillMaxWidth()) { - Row(Modifier.navClickable { navController -> - navController.navigate(navigateUri) - }) { - PreviewableAsyncImage(imageUrl = imageUrl, placeholderType = imagePlaceholder, modifier = Modifier.size(56.dp)) - Text(title, fontSize = 13.sp, lineHeight = 18.sp, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.align( - Alignment.CenterVertically).padding(horizontal = 8.dp)) + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.compositeSurfaceElevation( + 3.dp + ) + ), modifier = Modifier + .height(56.dp) + .fillMaxWidth() + ) { + Row(Modifier.navClickable { navController -> + navController.navigate(navigateUri) + }) { + PreviewableAsyncImage( + imageUrl = imageUrl, + placeholderType = imagePlaceholder, + modifier = Modifier.size(56.dp) + ) + Text( + title, + fontSize = 13.sp, + lineHeight = 18.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .align( + Alignment.CenterVertically + ) + .padding(horizontal = 8.dp) + .fillMaxWidth(), + + ) + } } - } } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_plans/BenefitListComponentBinder.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_plans/BenefitListComponentBinder.kt index 2053c05e..1312da03 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_plans/BenefitListComponentBinder.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_plans/BenefitListComponentBinder.kt @@ -33,13 +33,11 @@ fun BenefitListComponentBinder( Row(Modifier.padding(16.dp)) { MediumText(text = stringResource(id = R.string.plan_includes)) } - Surface( tonalElevation = 8.dp, modifier = Modifier .height(1.dp) .fillMaxWidth() ) {} - item.benefitsList.forEach { benefit -> Row( Modifier diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_plans/FallbackPlanComponentBinder.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_plans/FallbackPlanComponentBinder.kt new file mode 100644 index 00000000..a285ce03 --- /dev/null +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_plans/FallbackPlanComponentBinder.kt @@ -0,0 +1,45 @@ +package bruhcollective.itaysonlab.jetispot.ui.dac.components_plans + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import bruhcollective.itaysonlab.jetispot.ui.ext.compositeSurfaceElevation +import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText +import com.spotify.planoverview.v1.FallbackPlanComponent +import bruhcollective.itaysonlab.jetispot.R + +@Composable +fun FallbackPlanComponentBinder( + item: FallbackPlanComponent +) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.compositeSurfaceElevation( + 3.dp + ) + ), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + modifier = Modifier + .padding(top = 12.dp) + .padding(horizontal = 16.dp) + .fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + MediumText( + modifier = Modifier, + text = item.name + ) + Divider(modifier = Modifier.padding(top = 6.dp, bottom = 6.dp)) + MediumText( + modifier = Modifier, + text = item.description + ) + Text(stringResource(id = R.string.plan_overview_fallback_plan)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/HubBinder.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/HubBinder.kt index 61003832..2fb16473 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/HubBinder.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/HubBinder.kt @@ -2,6 +2,8 @@ package bruhcollective.itaysonlab.jetispot.ui.hub import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Divider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -10,6 +12,8 @@ import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubComponent import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem import bruhcollective.itaysonlab.jetispot.ui.hub.components.* +//TODO: FIX UNSUPPORTED ID IN LISTENING HISTORY - BOBBYESP + @Composable fun HubBinder ( item: HubItem, @@ -58,6 +62,11 @@ fun HubBinder ( HubComponent.PodcastTopics -> PodcastTopicsStrip(item) HubComponent.OutlinedButton -> OutlineButton(item) + + HubComponent.HistoryPlaylist -> PlaylistTrackRowLarger(item) + HubComponent.HistoryDivider -> Divider(modifier = Modifier.padding(start = 14.dp, end = 14.dp).height(2.dp)) + + //TODO: Keep adding components searching them in the API (Thunder client) HubComponent.EmptySpace, HubComponent.Ignored -> {} else -> { diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/HubEventHandler.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/HubEventHandler.kt index 645ae505..55be9a4b 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/HubEventHandler.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/HubEventHandler.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubEvent import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem +import bruhcollective.itaysonlab.jetispot.core.util.Log import bruhcollective.itaysonlab.jetispot.ui.navigation.NavigationController import bruhcollective.itaysonlab.jetispot.ui.shared.navAndHubClickable import bruhcollective.itaysonlab.jetispot.ui.shared.navClickable @@ -18,7 +19,9 @@ object HubEventHandler { navController.navigate(event.data.uri) } } + is HubEvent.PlayFromContext -> delegate.play(event.data) + HubEvent.Unknown -> {} } } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/AlbumHeader.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/AlbumHeader.kt index af33b73a..f72ed607 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/AlbumHeader.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/AlbumHeader.kt @@ -33,78 +33,100 @@ import kotlinx.coroutines.launch @Composable fun AlbumHeader( - item: HubItem + item: HubItem ) { - val darkTheme = isSystemInDarkTheme() - val dominantColor = remember { mutableStateOf(Color.Transparent) } - val dominantColorAsBg = animateColorAsState(dominantColor.value) - val delegate = LocalHubScreenDelegate.current + val darkTheme = isSystemInDarkTheme() + val dominantColor = remember { mutableStateOf(Color.Transparent) } + val dominantColorAsBg = animateColorAsState(dominantColor.value) + val delegate = LocalHubScreenDelegate.current - LaunchedEffect(Unit) { - launch { - if (dominantColor.value != Color.Transparent) return@launch - dominantColor.value = delegate.calculateDominantColor(item.images?.main?.uri.toString(), darkTheme) + LaunchedEffect(Unit) { + launch { + if (dominantColor.value != Color.Transparent) return@launch + dominantColor.value = + delegate.calculateDominantColor(item.images?.main?.uri.toString(), darkTheme) + } } - } - Column(modifier = Modifier - .fillMaxHeight() - .background( - brush = Brush.verticalGradient( - colors = listOf(dominantColorAsBg.value, Color.Transparent) - ) - ) - .padding(top = 16.dp) - .statusBarsPadding()) { + Column( + modifier = Modifier + .fillMaxHeight() + .background( + brush = Brush.verticalGradient( + colors = listOf(dominantColorAsBg.value, Color.Transparent) + ) + ) + .padding(top = 16.dp) + .statusBarsPadding() + ) { - PreviewableAsyncImage(item.images?.main?.uri, item.images?.main?.placeholder, modifier = Modifier - .size((LocalConfiguration.current.screenWidthDp * 0.7).dp) - .align(Alignment.CenterHorizontally) - .padding(bottom = 8.dp)) + PreviewableAsyncImage( + item.images?.main?.uri, item.images?.main?.placeholder, modifier = Modifier + .size((LocalConfiguration.current.screenWidthDp * 0.7).dp) + .align(Alignment.CenterHorizontally) + .padding(bottom = 8.dp) + ) - MediumText(text = item.text!!.title!!, fontSize = 21.sp, modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 8.dp)) + MediumText( + text = item.text!!.title!!, fontSize = 21.sp, modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 8.dp) + ) - if (item.metadata!!.album!!.artists.size == 1) { - // large - Row(modifier = Modifier - .navClickable(enableRipple = false) { navController -> - HubEventHandler.handle( - navController, - delegate, - HubEvent.NavigateToUri(NavigateUri(item.metadata.album!!.artists[0].uri!!)) - ) - } - .padding(horizontal = 16.dp) - .padding(vertical = 12.dp)) { - AsyncImage(model = item.metadata.album!!.artists.first().images!![0].uri, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier - .clip(CircleShape) - .size(32.dp)) - MediumText(text = item.metadata.album.artists.first().name!!, fontSize = 13.sp, modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 12.dp)) - } - } else { - Row(Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - item.metadata.album!!.artists.forEachIndexed { idx, artist -> - MediumText(text = artist.name!!, fontSize = 13.sp, modifier = Modifier.navClickable(enableRipple = false) { navController -> - HubEventHandler.handle( - navController, - delegate, - HubEvent.NavigateToUri(NavigateUri(artist.uri!!)) - ) - }) + if (item.metadata!!.album!!.artists.size == 1) { + // large + Row(modifier = Modifier + .navClickable(enableRipple = false) { navController -> + HubEventHandler.handle( + navController, + delegate, + HubEvent.NavigateToUri(NavigateUri(item.metadata.album!!.artists[0].uri!!)) + ) + } + .padding(horizontal = 16.dp) + .padding(vertical = 12.dp)) { + AsyncImage( + model = item.metadata.album!!.artists.first().images!![0].uri, + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .size(32.dp) + ) + MediumText( + text = item.metadata.album.artists.first().name!!, + fontSize = 13.sp, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 12.dp) + ) + } + } else { + Row(Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + item.metadata.album!!.artists.forEachIndexed { idx, artist -> + MediumText( + text = artist.name!!, + fontSize = 13.sp, + modifier = Modifier.navClickable(enableRipple = false) { navController -> + HubEventHandler.handle( + navController, + delegate, + HubEvent.NavigateToUri(NavigateUri(artist.uri!!)) + ) + }) - if (idx != item.metadata.album.artists.lastIndex) { - MediumText(text = " • ", fontSize = 13.sp) - } + if (idx != item.metadata.album.artists.lastIndex) { + MediumText(text = " • ", fontSize = 13.sp) + } + } + } } - } - } - Subtext(text = "${item.metadata.album!!.type} • ${item.metadata.album.year}", modifier = Modifier.padding(horizontal = 16.dp)) + Subtext( + text = "${item.metadata.album!!.type} • ${item.metadata.album.year}", + modifier = Modifier.padding(horizontal = 16.dp) + ) - EntityActionStrip(delegate, item) - } + EntityActionStrip(delegate, item) + } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/AlbumTrackRow.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/AlbumTrackRow.kt index c9f848c6..197ac6a2 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/AlbumTrackRow.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/AlbumTrackRow.kt @@ -14,18 +14,26 @@ import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext @Composable fun AlbumTrackRow( - item: HubItem + item: HubItem ) { - Column(Modifier.clickableHub(item).fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)) { - var drawnTitle = false + Column( + Modifier + .clickableHub(item) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + ) { + var drawnTitle = false - if (!item.text?.title.isNullOrEmpty()) { - drawnTitle = true - MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal) - } + if (!item.text?.title.isNullOrEmpty()) { + drawnTitle = true + MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal) + } - if (!item.text?.subtitle.isNullOrEmpty()) { - Subtext(item.text!!.subtitle!!, modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp)) + if (!item.text?.subtitle.isNullOrEmpty()) { + Subtext( + item.text!!.subtitle!!, + modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp) + ) + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ArtistPinnedItem.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ArtistPinnedItem.kt index 45466834..ae26df91 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ArtistPinnedItem.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ArtistPinnedItem.kt @@ -13,20 +13,30 @@ import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage -import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext @Composable -fun ArtistPinnedItem ( - item: HubItem +fun ArtistPinnedItem( + item: HubItem ) { - Row( - Modifier - .clickableHub(item) - .padding(horizontal = 16.dp, vertical = 2.dp)) { - PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier.size(72.dp).padding(vertical = 8.dp).padding(end = 16.dp)) - Column(Modifier.align(Alignment.CenterVertically)) { - MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal, modifier = Modifier.padding(bottom = 4.dp)) - Subtext(item.text.subtitle!!) + Row( + Modifier + .clickableHub(item) + .padding(horizontal = 16.dp, vertical = 2.dp) + ) { + PreviewableAsyncImage( + imageUrl = item.images?.main?.uri, + placeholderType = item.images?.main?.placeholder, + modifier = Modifier + .size(72.dp) + .padding(vertical = 8.dp) + .padding(end = 16.dp) + ) + Column(Modifier.align(Alignment.CenterVertically)) { + MediumText( + item.text!!.title!!, + fontWeight = FontWeight.Normal, + modifier = Modifier.padding(bottom = 4.dp) + ) + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ArtistTrackRow.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ArtistTrackRow.kt index 33e16ced..90db5cc8 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ArtistTrackRow.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ArtistTrackRow.kt @@ -18,29 +18,44 @@ import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext @Composable fun ArtistTrackRow( - item: HubItem + item: HubItem ) { - Row( - Modifier - .clickableHub(item) - .padding(horizontal = 16.dp, vertical = 12.dp)) { - Text(text = (item.custom!!["rowNumber"] as Double).toInt().toString(), modifier = Modifier - .align(Alignment.CenterVertically) - .padding(end = 16.dp)) + Row( + Modifier + .clickableHub(item) + .padding(horizontal = 16.dp, vertical = 12.dp) + ) { + Text( + text = (item.custom!!["rowNumber"] as Double).toInt().toString(), modifier = Modifier + .align(Alignment.CenterVertically) + .padding(end = 16.dp) + ) - PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier.align(Alignment.CenterVertically).size(48.dp)) + PreviewableAsyncImage( + imageUrl = item.images?.main?.uri, + placeholderType = item.images?.main?.placeholder, + modifier = Modifier + .align(Alignment.CenterVertically) + .size(48.dp) + ) - Column(Modifier.align(Alignment.CenterVertically).padding(start = 16.dp)) { - var drawnTitle = false + Column( + Modifier + .align(Alignment.CenterVertically) + .padding(start = 16.dp)) { + var drawnTitle = false - if (!item.text?.title.isNullOrEmpty()) { - drawnTitle = true - MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal) - } + if (!item.text?.title.isNullOrEmpty()) { + drawnTitle = true + MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal) + } - if (!item.text?.subtitle.isNullOrEmpty()) { - Subtext(item.text!!.subtitle!!, modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp)) - } + if (!item.text?.subtitle.isNullOrEmpty()) { + Subtext( + item.text!!.subtitle!!, + modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp) + ) + } + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/Carousel.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/Carousel.kt index 30f30d0d..4fad3b5e 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/Carousel.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/Carousel.kt @@ -18,20 +18,30 @@ import bruhcollective.itaysonlab.jetispot.ui.hub.LocalHubScreenDelegate @Composable fun Carousel( - item: HubItem, + item: HubItem, ) { - val isSurroundedWithPadding = LocalHubScreenDelegate.current.isSurroundedWithPadding() - - Column(Modifier.padding(vertical = if (isSurroundedWithPadding) 0.dp else 8.dp)) { - if (item.text != null) { - Text(text = item.text.title!!, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 16.sp, - modifier = Modifier.padding(horizontal = if (isSurroundedWithPadding) 0.dp else 16.dp).padding(bottom = 12.dp)) - } + val isSurroundedWithPadding = LocalHubScreenDelegate.current.isSurroundedWithPadding() + + Column(Modifier.padding(vertical = if (isSurroundedWithPadding) 0.dp else 8.dp)) { + if (item.text != null) { + Text( + text = item.text.title!!, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + modifier = Modifier + .padding(horizontal = if (isSurroundedWithPadding) 0.dp else 16.dp) + .padding(bottom = 12.dp) + ) + } - LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.padding(horizontal = if (isSurroundedWithPadding) 0.dp else 16.dp)) { - items(item.children ?: listOf()) { cItem -> - HubBinder(cItem) - } + LazyRow( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(horizontal = if (isSurroundedWithPadding) 0.dp else 16.dp) + ) { + items(item.children ?: listOf()) { cItem -> + HubBinder(cItem) + } + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/CollectionHeader.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/CollectionHeader.kt index 3fca9db2..34f29218 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/CollectionHeader.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/CollectionHeader.kt @@ -54,7 +54,7 @@ fun CollectionHeader( .padding(horizontal = 16.dp) .padding(top = 8.dp)) - Subtext(text = "${item.custom!!["count"]} songs", fontSize = 14.sp, modifier = Modifier + Subtext(text = "${item.custom!!["count"]} " + stringResource(id = R.string.songs), fontSize = 14.sp, modifier = Modifier .padding(horizontal = 16.dp) .padding(top = 2.dp)) diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/EpisodeListItem.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/EpisodeListItem.kt index dafe7914..3f945f9a 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/EpisodeListItem.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/EpisodeListItem.kt @@ -32,94 +32,134 @@ import java.text.DateFormat import java.util.* @Composable -fun EpisodeListItem ( - item: HubItem +fun EpisodeListItem( + item: HubItem ) { - val episode = remember { item.custom!!["episode"] as Metadata.Episode } - val imageUrl = remember { SpUtils.getImageUrl(episode.coverImage.imageList.first { it.size == Metadata.Image.Size.DEFAULT }.fileId) } - val formattedDuration = remember { DateUtils.formatElapsedTime(episode.duration / 1000L) } - val formattedPublishDate = remember { - DateFormat.getDateInstance().format(Calendar.getInstance().apply { - set(episode.publishTime.year, episode.publishTime.month, episode.publishTime.day, episode.publishTime.hour, episode.publishTime.minute) - }.time) - } - - Column( - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp)) { - Row { - PreviewableAsyncImage(imageUrl = imageUrl, placeholderType = "podcast", modifier = Modifier - .size(56.dp) - .clip(RoundedCornerShape(8.dp))) - Text(text = episode.name, modifier = Modifier - .padding(start = 16.dp) - .align(Alignment.CenterVertically), color = MaterialTheme.colorScheme.onSurface, maxLines = 2, overflow = TextOverflow.Ellipsis) + val episode = remember { item.custom!!["episode"] as Metadata.Episode } + val imageUrl = + remember { SpUtils.getImageUrl(episode.coverImage.imageList.first { it.size == Metadata.Image.Size.DEFAULT }.fileId) } + val formattedDuration = remember { DateUtils.formatElapsedTime(episode.duration / 1000L) } + val formattedPublishDate = remember { + DateFormat.getDateInstance().format(Calendar.getInstance().apply { + set( + episode.publishTime.year, + episode.publishTime.month, + episode.publishTime.day, + episode.publishTime.hour, + episode.publishTime.minute + ) + }.time) } - Spacer(Modifier.height(16.dp)) + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Row { + PreviewableAsyncImage( + imageUrl = imageUrl, placeholderType = "podcast", modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(8.dp)) + ) + Text( + text = episode.name, + modifier = Modifier + .padding(start = 16.dp) + .align(Alignment.CenterVertically), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } - Text(text = episode.description, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), fontSize = 13.sp, maxLines = 2, overflow = TextOverflow.Ellipsis) + Spacer(Modifier.height(16.dp)) - Spacer(Modifier.height(16.dp)) + Text( + text = episode.description, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + fontSize = 13.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) - Row { - if (episode.explicit) { - Icon(Icons.Rounded.Explicit, tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), contentDescription = null, modifier = Modifier - .size(16.dp) - .align(Alignment.CenterVertically)) - Text(text = " • ", color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), fontSize = 13.sp) - } + Spacer(Modifier.height(16.dp)) - Text(text = "$formattedDuration • $formattedPublishDate", color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), fontSize = 13.sp) - } + Row { + if (episode.explicit) { + Icon( + Icons.Rounded.Explicit, + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + contentDescription = null, + modifier = Modifier + .size(16.dp) + .align(Alignment.CenterVertically) + ) + Text( + text = " • ", + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + fontSize = 13.sp + ) + } - Spacer(Modifier.height(16.dp)) + Text( + text = "$formattedDuration • $formattedPublishDate", + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + fontSize = 13.sp + ) + } - Row { - IconButton(onClick = { /*TODO*/ }, - Modifier - .offset(y = 2.dp) - .align(Alignment.CenterVertically) - .size(28.dp)) { - Icon(Icons.Rounded.AddCircle, null) - } + Spacer(Modifier.height(16.dp)) - Spacer(Modifier.width(16.dp)) + Row { + IconButton( + onClick = { /*TODO*/ }, + Modifier + .offset(y = 2.dp) + .align(Alignment.CenterVertically) + .size(28.dp) + ) { + Icon(Icons.Rounded.AddCircle, null) + } - IconButton(onClick = { /*TODO*/ }, - Modifier - .offset(y = 2.dp) - .align(Alignment.CenterVertically) - .size(28.dp)) { - Icon(Icons.Rounded.Share, null) - } + Spacer(Modifier.width(16.dp)) - Spacer(Modifier.weight(1f)) + IconButton( + onClick = { /*TODO*/ }, + Modifier + .offset(y = 2.dp) + .align(Alignment.CenterVertically) + .size(28.dp) + ) { + Icon(Icons.Rounded.Share, null) + } - Box( - Modifier - .clickableHub(item) - .size(28.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary) - ) { - Icon( - imageVector = Icons.Rounded.PlayArrow, - tint = MaterialTheme.colorScheme.onPrimary, - contentDescription = null, - modifier = Modifier - .size(24.dp) - .align(Alignment.Center) - ) - } - } + Spacer(Modifier.weight(1f)) - Box( - Modifier - .padding(top = 16.dp) - .background(MaterialTheme.colorScheme.compositeSurfaceElevation(8.dp)) - .fillMaxWidth() - .height(1.dp)) {} - } + Box( + Modifier + .clickableHub(item) + .size(28.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary) + ) { + Icon( + imageVector = Icons.Rounded.PlayArrow, + tint = MaterialTheme.colorScheme.onPrimary, + contentDescription = null, + modifier = Modifier + .size(24.dp) + .align(Alignment.Center) + ) + } + } + + Box( + Modifier + .padding(top = 16.dp) + .background(MaterialTheme.colorScheme.compositeSurfaceElevation(8.dp)) + .fillMaxWidth() + .height(1.dp) + ) {} + } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/FindCard.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/FindCard.kt index 3eacb245..e2ffc3fb 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/FindCard.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/FindCard.kt @@ -17,12 +17,29 @@ import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage @Composable fun FindCard( - item: HubItem + item: HubItem ) { - Card(modifier = Modifier.height(100.dp).fillMaxWidth().clickableHub(item)) { - Box { - PreviewableAsyncImage(imageUrl = item.images?.background?.uri, placeholderType = item.images?.background?.placeholder, modifier = Modifier.fillMaxSize()) - Text(item.text!!.title!!, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Bold, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.align(Alignment.TopStart).padding(12.dp)) + Card(modifier = Modifier + .height(100.dp) + .fillMaxWidth() + .clickableHub(item)) { + Box { + PreviewableAsyncImage( + imageUrl = item.images?.background?.uri, + placeholderType = item.images?.background?.placeholder, + modifier = Modifier.fillMaxSize() + ) + Text( + item.text!!.title!!, + color = Color.White, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .align(Alignment.TopStart) + .padding(12.dp) + ) + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/GridMediumCard.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/GridMediumCard.kt index 5f69c6b8..e48c1931 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/GridMediumCard.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/GridMediumCard.kt @@ -19,26 +19,58 @@ import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext @Composable fun GridMediumCard( - item: HubItem + item: HubItem ) { - val size = 160.dp - - Column(Modifier.fillMaxWidth().clickableHub(item)) { - var drawnTitle = false + val size = 160.dp - PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier.size(size).clip( - RoundedCornerShape(if (item.images?.main?.isRounded == true) 12.dp else 0.dp) - ).align(Alignment.CenterHorizontally)) + Column( + Modifier + .fillMaxWidth() + .clickableHub(item) + ) { + var drawnTitle = false - if (!item.text?.title.isNullOrEmpty()) { - drawnTitle = true - MediumText(item.text!!.title!!, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth().padding(top = 8.dp).padding(horizontal = 16.dp)) - } + PreviewableAsyncImage( + imageUrl = item.images?.main?.uri, + placeholderType = item.images?.main?.placeholder, + modifier = Modifier + .size(size) + .clip( + RoundedCornerShape(if (item.images?.main?.isRounded == true) 12.dp else 0.dp) + ) + .align(Alignment.CenterHorizontally) + ) + + if (!item.text?.title.isNullOrEmpty()) { + drawnTitle = true + MediumText( + item.text!!.title!!, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .padding(horizontal = 16.dp) + ) + } - if (!item.text?.subtitle.isNullOrEmpty()) { - Subtext(item.text!!.subtitle!!, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth().padding(top = if (drawnTitle) 2.dp else 8.dp, bottom = 12.dp).padding(horizontal = 16.dp)) - } else if (!item.text?.description.isNullOrEmpty()) { - Subtext(item.text!!.description!!, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth().padding(top = if (drawnTitle) 2.dp else 8.dp, bottom = 12.dp).padding(horizontal = 16.dp)) + if (!item.text?.subtitle.isNullOrEmpty()) { + Subtext( + item.text!!.subtitle!!, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = if (drawnTitle) 2.dp else 8.dp, bottom = 12.dp) + .padding(horizontal = 16.dp) + ) + } else if (!item.text?.description.isNullOrEmpty()) { + Subtext( + item.text!!.description!!, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = if (drawnTitle) 2.dp else 8.dp, bottom = 12.dp) + .padding(horizontal = 16.dp) + ) + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/HomeSectionHeader.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/HomeSectionHeader.kt index 1c9342db..0c60479f 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/HomeSectionHeader.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/HomeSectionHeader.kt @@ -15,10 +15,20 @@ import bruhcollective.itaysonlab.jetispot.ui.hub.HubScreenDelegate import bruhcollective.itaysonlab.jetispot.ui.hub.LocalHubScreenDelegate @Composable -fun HomeSectionHeader ( - text: HubText, +fun HomeSectionHeader( + text: HubText, ) { - Box(Modifier.padding(vertical = 8.dp).padding(horizontal = if (LocalHubScreenDelegate.current.isSurroundedWithPadding()) 0.dp else 16.dp)) { - Text(text = text.title!!, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 21.sp, modifier = Modifier.align(Alignment.CenterStart)) - } + Box( + Modifier + .padding(vertical = 8.dp) + .padding(horizontal = if (LocalHubScreenDelegate.current.isSurroundedWithPadding()) 0.dp else 16.dp) + ) { + Text( + text = text.title!!, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold, + fontSize = 21.sp, + modifier = Modifier.align(Alignment.CenterStart) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/HomeSectionLargeHeader.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/HomeSectionLargeHeader.kt index ff1fa1fb..a417d86a 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/HomeSectionLargeHeader.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/HomeSectionLargeHeader.kt @@ -18,17 +18,27 @@ import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage import bruhcollective.itaysonlab.jetispot.ui.shared.SubtextOverline @Composable -fun HomeSectionLargeHeader ( - item: HubItem +fun HomeSectionLargeHeader( + item: HubItem ) { - Row(Modifier.padding(vertical = 8.dp).clickableHub(item)) { - PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier - .size(48.dp) - .clip(CircleShape)) + Row( + Modifier + .padding(vertical = 8.dp) + .clickableHub(item)) { + PreviewableAsyncImage( + imageUrl = item.images?.main?.uri, + placeholderType = item.images?.main?.placeholder, + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + ) - Column(Modifier.padding(horizontal = 12.dp).align(Alignment.CenterVertically)) { - SubtextOverline(item.text!!.subtitle!!.uppercase(), modifier = Modifier) - MediumText(item.text.title!!, modifier = Modifier.padding(top = 2.dp), fontSize = 21.sp) + Column( + Modifier + .padding(horizontal = 12.dp) + .align(Alignment.CenterVertically)) { + SubtextOverline(item.text!!.subtitle!!.uppercase(), modifier = Modifier) + MediumText(item.text.title!!, modifier = Modifier.padding(top = 2.dp), fontSize = 21.sp) + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ImageRow.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ImageRow.kt index 63418e99..8756134a 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ImageRow.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ImageRow.kt @@ -16,15 +16,26 @@ import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage @Composable fun ImageRow( - item: HubItem + item: HubItem ) { - Row(Modifier.clickableHub(item).fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)) { - PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier - .size(42.dp) - .clip(CircleShape)) + Row( + Modifier + .clickableHub(item) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp)) { + PreviewableAsyncImage( + imageUrl = item.images?.main?.uri, + placeholderType = item.images?.main?.placeholder, + modifier = Modifier + .size(42.dp) + .clip(CircleShape) + ) - Column(Modifier.padding(horizontal = 12.dp).align(Alignment.CenterVertically)) { - MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal, fontSize = 18.sp) + Column( + Modifier + .padding(horizontal = 12.dp) + .align(Alignment.CenterVertically)) { + MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal, fontSize = 18.sp) + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/LargerRow.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/LargerRow.kt index 60c910f6..6804ca0b 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/LargerRow.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/LargerRow.kt @@ -16,17 +16,29 @@ import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext @Composable -fun LargerRow ( - item: HubItem +fun LargerRow( + item: HubItem ) { - Row( - Modifier - .clickableHub(item) - .padding(horizontal = 16.dp, vertical = 2.dp)) { - PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier.size(72.dp).padding(end = 16.dp).padding(vertical = 8.dp)) - Column(Modifier.align(Alignment.CenterVertically)) { - MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal, modifier = Modifier.padding(bottom = 4.dp)) - Subtext(item.text.subtitle!!) + Row( + Modifier + .clickableHub(item) + .padding(horizontal = 16.dp, vertical = 2.dp) + ) { + PreviewableAsyncImage( + imageUrl = item.images?.main?.uri, + placeholderType = item.images?.main?.placeholder, + modifier = Modifier + .size(72.dp) + .padding(end = 16.dp) + .padding(vertical = 8.dp) + ) + Column(Modifier.align(Alignment.CenterVertically)) { + MediumText( + item.text!!.title!!, + fontWeight = FontWeight.Normal, + modifier = Modifier.padding(bottom = 4.dp) + ) + Subtext(item.text.subtitle!!) + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/LikedSongsRow.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/LikedSongsRow.kt index 95687b93..ba55133a 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/LikedSongsRow.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/LikedSongsRow.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import bruhcollective.itaysonlab.jetispot.SpApp import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubEvent import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem import bruhcollective.itaysonlab.jetispot.ui.hub.LocalHubScreenDelegate @@ -24,6 +25,7 @@ import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext import kotlinx.coroutines.launch +import bruhcollective.itaysonlab.jetispot.R import xyz.gianlu.librespot.metadata.ArtistId @Composable @@ -36,7 +38,7 @@ fun LikedSongsRow( LaunchedEffect(Unit) { launch { val count = delegate.getLikedSongsCount(ArtistId.fromBase62((item.events!!.click as HubEvent.NavigateToUri).data.uri.split(":").last()).hexId()) - likedSongsInfo.value = if (count == 0) "" else "$count songs by ${item.metadata!!.artist!!.name}" + likedSongsInfo.value = if (count == 0) "" else "$count " + SpApp.context.getString(R.string.songs_by) + " ${item.metadata!!.artist!!.name}" } } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/MediumCard.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/MediumCard.kt index 40518aca..afdf4813 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/MediumCard.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/MediumCard.kt @@ -17,26 +17,41 @@ import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext @Composable fun MediumCard( - item: HubItem + item: HubItem ) { - val size = 160.dp - - Column(Modifier.width(size).clickableHub(item)) { - var drawnTitle = false + val size = 160.dp - PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier.size(size).clip( - RoundedCornerShape(if (item.images?.main?.isRounded == true) 12.dp else 0.dp) - )) + Column( + Modifier + .width(size) + .clickableHub(item)) { + var drawnTitle = false - if (!item.text?.title.isNullOrEmpty()) { - drawnTitle = true - MediumText(item.text!!.title!!, modifier = Modifier.padding(top = 8.dp)) - } + PreviewableAsyncImage( + imageUrl = item.images?.main?.uri, + placeholderType = item.images?.main?.placeholder, + modifier = Modifier + .size(size) + .clip( + RoundedCornerShape(if (item.images?.main?.isRounded == true) 12.dp else 0.dp) + ) + ) + + if (!item.text?.title.isNullOrEmpty()) { + drawnTitle = true + MediumText(item.text!!.title!!, modifier = Modifier.padding(top = 8.dp)) + } - if (!item.text?.subtitle.isNullOrEmpty()) { - Subtext(item.text!!.subtitle!!, modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp)) - } else if (!item.text?.description.isNullOrEmpty()) { - Subtext(item.text!!.description!!, modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp)) + if (!item.text?.subtitle.isNullOrEmpty()) { + Subtext( + item.text!!.subtitle!!, + modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp) + ) + } else if (!item.text?.description.isNullOrEmpty()) { + Subtext( + item.text!!.description!!, + modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp) + ) + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/OutlineButton.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/OutlineButton.kt index af740d37..a1502623 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/OutlineButton.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/OutlineButton.kt @@ -20,10 +20,25 @@ import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText fun OutlineButton( item: HubItem ) { - Box(Modifier.clickableHub(item).padding(16.dp)) { + Box( + Modifier + .clickableHub(item) + .padding(16.dp) + ) { Row(Modifier.align(Alignment.Center)) { - MediumText(text = item.text?.title!!, modifier = Modifier.align(Alignment.CenterVertically)) - Icon(Icons.Rounded.ChevronRight, null, tint = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(start = 2.dp).size(20.dp).align(Alignment.CenterVertically)) + MediumText( + text = item.text?.title!!, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Icon( + Icons.Rounded.ChevronRight, + null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .padding(start = 2.dp) + .size(20.dp) + .align(Alignment.CenterVertically) + ) } } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PlaylistHeader.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PlaylistHeader.kt index 342b04b2..86c35c41 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PlaylistHeader.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PlaylistHeader.kt @@ -23,8 +23,9 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import bruhcollective.itaysonlab.jetispot.SpApp import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem -import bruhcollective.itaysonlab.jetispot.ui.hub.HubScreenDelegate +import bruhcollective.itaysonlab.jetispot.R import bruhcollective.itaysonlab.jetispot.ui.hub.LocalHubScreenDelegate import bruhcollective.itaysonlab.jetispot.ui.hub.components.essentials.EntityActionStrip import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText @@ -35,166 +36,168 @@ import kotlinx.coroutines.launch @Composable fun PlaylistHeader( - item: HubItem + item: HubItem ) { - val darkTheme = isSystemInDarkTheme() - val dominantColor = remember { mutableStateOf(Color.Transparent) } - val dominantColorAsBg = animateColorAsState(dominantColor.value) - val delegate = LocalHubScreenDelegate.current - - LaunchedEffect(Unit) { - launch { - if (dominantColor.value != Color.Transparent) return@launch - dominantColor.value = - delegate.calculateDominantColor(item.images?.main?.uri.toString(), darkTheme) + val darkTheme = isSystemInDarkTheme() + val dominantColor = remember { mutableStateOf(Color.Transparent) } + val dominantColorAsBg = animateColorAsState(dominantColor.value) + val delegate = LocalHubScreenDelegate.current + + LaunchedEffect(Unit) { + launch { + if (dominantColor.value != Color.Transparent) return@launch + dominantColor.value = + delegate.calculateDominantColor(item.images?.main?.uri.toString(), darkTheme) + } } - } - Column( - Modifier - .fillMaxHeight() - .fillMaxWidth() - .background( - brush = Brush.verticalGradient( - colors = listOf(dominantColorAsBg.value, Color.Transparent) + Column( + Modifier + .fillMaxHeight() + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf(dominantColorAsBg.value, Color.Transparent) + ) + ) + .padding(top = 16.dp) + .statusBarsPadding() + ) { + PreviewableAsyncImage( + item.images?.main?.uri, "playlist", modifier = Modifier + .size((LocalConfiguration.current.screenWidthDp * 0.7).dp) + .align(Alignment.CenterHorizontally) + .padding(bottom = 8.dp) + ) + + MediumText( + text = item.text?.title!!, fontSize = 21.sp, modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 8.dp) ) - ) - .padding(top = 16.dp) - .statusBarsPadding() - ) { - PreviewableAsyncImage(item.images?.main?.uri, "playlist", modifier = Modifier - .size((LocalConfiguration.current.screenWidthDp * 0.7).dp) - .align(Alignment.CenterHorizontally) - .padding(bottom = 8.dp)) - - MediumText( - text = item.text?.title!!, fontSize = 21.sp, modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 8.dp) - ) - - if (!item.text.subtitle.isNullOrEmpty()) { - Text( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), - fontSize = 12.sp, - lineHeight = 18.sp, - text = item.text.subtitle, modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 8.dp) - ) - } - PlaylistHeaderAdditionalInfo(item.custom) - EntityActionStrip(delegate, item) - } + if (!item.text.subtitle.isNullOrEmpty()) { + Text( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + fontSize = 12.sp, + lineHeight = 18.sp, + text = item.text.subtitle, modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 8.dp) + ) + } + + PlaylistHeaderAdditionalInfo(item.custom) + EntityActionStrip(delegate, item) + } } @Composable fun LargePlaylistHeader( - item: HubItem + item: HubItem ) { - val delegate = LocalHubScreenDelegate.current + val delegate = LocalHubScreenDelegate.current + + Column { + Box( + Modifier + .fillMaxWidth() + .height(240.dp) + ) { + AsyncImage( + model = item.images?.main?.uri, + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .fillMaxSize() + ) - Column { - Box( - Modifier - .fillMaxWidth() - .height(240.dp) - ) { - AsyncImage( - model = item.images?.main?.uri, - contentScale = ContentScale.Crop, - contentDescription = null, - modifier = Modifier - .fillMaxSize() - ) - - Box( - Modifier - .background( - brush = Brush.verticalGradient( - colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.7f)) + Box( + Modifier + .background( + brush = Brush.verticalGradient( + colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.7f)) + ) + ) + .fillMaxSize() ) - ) - .fillMaxSize() - ) - - MediumText( - text = item.text?.title!!, - fontSize = 48.sp, - lineHeight = 52.sp, - maxLines = 2, - modifier = Modifier - .align(Alignment.BottomStart) - .padding(horizontal = 16.dp) - .padding(bottom = 8.dp) - ) - } - if (!item.text?.subtitle.isNullOrEmpty()) { - Text( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), - fontSize = 12.sp, - lineHeight = 18.sp, - text = item.text?.subtitle!!, modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 16.dp) - ) - } + MediumText( + text = item.text?.title!!, + fontSize = 48.sp, + lineHeight = 52.sp, + maxLines = 2, + modifier = Modifier + .align(Alignment.BottomStart) + .padding(horizontal = 16.dp) + .padding(bottom = 8.dp) + ) + } + + if (!item.text?.subtitle.isNullOrEmpty()) { + Text( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + fontSize = 12.sp, + lineHeight = 18.sp, + text = item.text?.subtitle!!, modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 16.dp) + ) + } - PlaylistHeaderAdditionalInfo(item.custom) - EntityActionStrip(delegate, item) - } + PlaylistHeaderAdditionalInfo(item.custom) + EntityActionStrip(delegate, item) + } } @Composable fun PlaylistHeaderAdditionalInfo( - custom: Map? + custom: Map? ) { - custom ?: return - - val ownerPic = remember(custom) { custom["owner_pic"] as String } - val ownerName = remember(custom) { custom["owner_name"] as String } - val likesCount = remember(custom) { custom["likes_count"] as Long } - val totalDuration = remember(custom) { custom["total_duration"] as String } - - Spacer(modifier = Modifier.height(12.dp)) - - Row(Modifier - .navClickable( - enableRipple = false - ) { navController -> navController.navigate(custom["owner_username"] as String) } - .fillMaxWidth() - .padding(horizontal = 16.dp)) { - PreviewableAsyncImage( - imageUrl = ownerPic, placeholderType = "user", modifier = Modifier - .clip(CircleShape) - .size(32.dp) - ) - MediumText( - text = ownerName, fontSize = 13.sp, modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 12.dp) - ) - } - - Spacer(modifier = Modifier.height(12.dp)) - - Row( - Modifier + custom ?: return + + val ownerPic = remember(custom) { custom["owner_pic"] as String } + val ownerName = remember(custom) { custom["owner_name"] as String } + val likesCount = remember(custom) { custom["likes_count"] as Long } + val totalDuration = remember(custom) { custom["total_duration"] as String } + + Spacer(modifier = Modifier.height(12.dp)) + + Row(Modifier + .navClickable( + enableRipple = false + ) { navController -> navController.navigate(custom["owner_username"] as String) } .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - Icon(Icons.Rounded.Language, contentDescription = null, modifier = Modifier.size(26.dp)) - Text( - text = "$likesCount likes • $totalDuration", - fontSize = 12.sp, - maxLines = 1, - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 8.dp) - ) - } - - Spacer(modifier = Modifier.height(6.dp)) + .padding(horizontal = 16.dp)) { + PreviewableAsyncImage( + imageUrl = ownerPic, placeholderType = "user", modifier = Modifier + .clip(CircleShape) + .size(32.dp) + ) + MediumText( + text = ownerName, fontSize = 13.sp, modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 12.dp) + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Icon(Icons.Rounded.Language, contentDescription = null, modifier = Modifier.size(26.dp)) + Text( + text = "$likesCount " + SpApp.context.getString(R.string.likes_dot) + totalDuration, + fontSize = 12.sp, + maxLines = 1, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 8.dp) + ) + } + + Spacer(modifier = Modifier.height(6.dp)) } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PlaylistTrackRowLarger.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PlaylistTrackRowLarger.kt new file mode 100644 index 00000000..0cc7ad0b --- /dev/null +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PlaylistTrackRowLarger.kt @@ -0,0 +1,67 @@ +package bruhcollective.itaysonlab.jetispot.ui.hub.components + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem +import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub +import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText +import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage +import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext + +@Composable +fun PlaylistTrackRowLarger( + item: HubItem +) { + val artists = remember(item) { + if (!item.text?.subtitle.isNullOrEmpty()) { + item.text!!.subtitle!! + } else if (item.custom?.get("artists") != null) { + (item.custom["artists"] as List>).joinToString { it["name"].toString() } + } else { + "" + } + } + + Row( + Modifier + .clickableHub(item) + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + PreviewableAsyncImage( + imageUrl = item.images?.main?.uri, + placeholderType = "track", + modifier = Modifier + .align( + Alignment.CenterVertically + ) + .size(72.dp) + ) + + Column( + Modifier + .padding( + start = 16.dp + ) + .align(Alignment.CenterVertically) + ) { + var drawnTitle = false + + if (!item.text?.title.isNullOrEmpty()) { + drawnTitle = true + MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal) + } + + Subtext( + artists, + modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp), + maxLines = 1, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PodcastTopicsStrip.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PodcastTopicsStrip.kt index 54e86f85..e20cafb6 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PodcastTopicsStrip.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PodcastTopicsStrip.kt @@ -21,39 +21,49 @@ import com.spotify.podcastextensions.proto.PodcastTopics @OptIn(ExperimentalMaterial3Api::class) @Composable -fun PodcastTopicsStrip ( - item: HubItem +fun PodcastTopicsStrip( + item: HubItem ) { - val navController = LocalNavigationController.current - val topics = remember { item.custom!!["topics"] as PodcastTopics } - val rating = remember { item.custom!!["ratings"] as PodcastRating } + val navController = LocalNavigationController.current + val topics = remember { item.custom!!["topics"] as PodcastTopics } + val rating = remember { item.custom!!["ratings"] as PodcastRating } - LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(horizontal = 16.dp)) { - item { - ElevatedSuggestionChip(onClick = { - // TODO - }, icon = { - Icon(imageVector = Icons.Rounded.Star, contentDescription = null) - }, label = { - Text(text = "${String.format("%.2f", rating.averageRating.average)} (${rating.averageRating.totalRatings})") - }) - } + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 16.dp) + ) { + item { + ElevatedSuggestionChip(onClick = { + // TODO + }, icon = { + Icon(imageVector = Icons.Rounded.Star, contentDescription = null) + }, label = { + Text( + text = "${ + String.format( + "%.2f", + rating.averageRating.average + ) + } (${rating.averageRating.totalRatings})" + ) + }) + } - items(topics.topicsList) { topic -> - PodcastTopic(topic = topic, onClick = navController::navigate) + items(topics.topicsList) { topic -> + PodcastTopic(topic = topic, onClick = navController::navigate) + } } - } } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun PodcastTopic ( - topic: PodcastTopic, - onClick: (String) -> Unit +private fun PodcastTopic( + topic: PodcastTopic, + onClick: (String) -> Unit ) { - ElevatedSuggestionChip(onClick = { - onClick(topic.uri) - }, label = { - Text(text = topic.title) - }) + ElevatedSuggestionChip(onClick = { + onClick(topic.uri) + }, label = { + Text(text = topic.title) + }) } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShortcutsCard.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShortcutsCard.kt index 5f3377c5..13674220 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShortcutsCard.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShortcutsCard.kt @@ -18,12 +18,34 @@ import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage @Composable fun ShortcutsCard( - item: HubItem + item: HubItem ) { - Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.compositeSurfaceElevation(3.dp)), modifier = Modifier.height(56.dp).fillMaxWidth()) { - Row(Modifier.clickableHub(item)) { - PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier.size(56.dp)) - Text(item.text!!.title!!, fontSize = 13.sp, lineHeight = 18.sp, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.align(Alignment.CenterVertically).padding(horizontal = 8.dp)) + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.compositeSurfaceElevation( + 3.dp + ) + ), modifier = Modifier + .height(56.dp) + .fillMaxWidth() + ) { + Row(Modifier.clickableHub(item)) { + PreviewableAsyncImage( + imageUrl = item.images?.main?.uri, + placeholderType = item.images?.main?.placeholder, + modifier = Modifier.size(56.dp) + ) + Text( + item.text!!.title!!, + fontSize = 13.sp, + lineHeight = 18.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(horizontal = 8.dp) + .fillMaxWidth() + ) + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShortcutsContainer.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShortcutsContainer.kt index 9333e4f7..b3262487 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShortcutsContainer.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShortcutsContainer.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem import bruhcollective.itaysonlab.jetispot.ui.hub.HubBinder +//Maybe Feed top shortcuts? + @Composable fun ShortcutsContainer ( children: List diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShowHeader.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShowHeader.kt index e15805a4..973d8fc3 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShowHeader.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShowHeader.kt @@ -21,53 +21,58 @@ import com.spotify.metadata.Metadata @Composable fun ShowHeader( - item: HubItem + item: HubItem ) { - val show = remember { item.custom!!["show"] as Metadata.Show } - val imageUrl = remember { SpUtils.getImageUrl(show.coverImage.imageList.first { it.size == Metadata.Image.Size.DEFAULT }.fileId) } + val show = remember { item.custom!!["show"] as Metadata.Show } + val imageUrl = + remember { SpUtils.getImageUrl(show.coverImage.imageList.first { it.size == Metadata.Image.Size.DEFAULT }.fileId) } - Column { - Box( - Modifier - .fillMaxWidth() - .height(240.dp) - ) { - AsyncImage( - model = imageUrl, - contentScale = ContentScale.Crop, - contentDescription = null, - modifier = Modifier - .fillMaxSize() - ) + Column { + Box( + Modifier + .fillMaxWidth() + .height(240.dp) + ) { + AsyncImage( + model = imageUrl, + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .fillMaxSize() + ) - Box( - Modifier - .background( - brush = Brush.verticalGradient( - colors = listOf(Color.Transparent, MaterialTheme.colorScheme.surface.copy(alpha = 0.9f), MaterialTheme.colorScheme.surface) + Box( + Modifier + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + MaterialTheme.colorScheme.surface.copy(alpha = 0.9f), + MaterialTheme.colorScheme.surface + ) + ) + ) + .fillMaxSize() ) - ) - .fillMaxSize() - ) - MediumText( - text = show.name, - fontSize = 48.sp, - lineHeight = 52.sp, - maxLines = 2, - modifier = Modifier - .align(Alignment.BottomStart) - .padding(horizontal = 16.dp) - .padding(bottom = 8.dp) - ) - } + MediumText( + text = show.name, + fontSize = 48.sp, + lineHeight = 52.sp, + maxLines = 2, + modifier = Modifier + .align(Alignment.BottomStart) + .padding(horizontal = 16.dp) + .padding(bottom = 8.dp) + ) + } - Text( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), - fontSize = 12.sp, - lineHeight = 18.sp, - text = show.description, modifier = Modifier - .padding(horizontal = 16.dp) - ) - } + Text( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + fontSize = 12.sp, + lineHeight = 18.sp, + text = show.description, modifier = Modifier + .padding(horizontal = 16.dp) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/SingleFocusCard.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/SingleFocusCard.kt index f73f07d8..4a1e2322 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/SingleFocusCard.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/SingleFocusCard.kt @@ -16,23 +16,36 @@ import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext @Composable -fun SingleFocusCard ( - item: HubItem +fun SingleFocusCard( + item: HubItem ) { - Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.compositeSurfaceElevation(3.dp)), modifier = Modifier - .height(120.dp) - .fillMaxWidth() - .clickableHub(item)) { - Row { - PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier - .fillMaxHeight() - .width(120.dp)) - Box(Modifier.fillMaxSize().padding(16.dp)) { - Column(Modifier.align(Alignment.TopStart)) { - MediumText(text = item.text!!.title!!) - Subtext(text = item.text.subtitle!!) + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.compositeSurfaceElevation( + 3.dp + ) + ), modifier = Modifier + .height(120.dp) + .fillMaxWidth() + .clickableHub(item) + ) { + Row { + PreviewableAsyncImage( + imageUrl = item.images?.main?.uri, + placeholderType = item.images?.main?.placeholder, + modifier = Modifier + .fillMaxHeight() + .width(120.dp) + ) + Box( + Modifier + .fillMaxSize() + .padding(16.dp)) { + Column(Modifier.align(Alignment.TopStart)) { + MediumText(text = item.text!!.title!!) + Subtext(text = item.text.subtitle!!) + } + } } - } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/TextRow.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/TextRow.kt index d45ea24c..13874451 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/TextRow.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/TextRow.kt @@ -11,7 +11,12 @@ import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubText @Composable fun TextRow( - text: HubText + text: HubText ) { - Text(text.title ?: text.description ?: "", color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (text.description == null) 1f else 0.7f), fontSize = 16.sp, modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp)) + Text( + text.title ?: text.description ?: "", + color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (text.description == null) 1f else 0.7f), + fontSize = 16.sp, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp) + ) } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/essentials/EntityActionStrip.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/essentials/EntityActionStrip.kt index 4e568a09..3e2056c8 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/essentials/EntityActionStrip.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/essentials/EntityActionStrip.kt @@ -20,48 +20,78 @@ import bruhcollective.itaysonlab.jetispot.ui.hub.HubScreenDelegate import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub @Composable -fun EntityActionStrip ( - delegate: HubScreenDelegate, - item: HubItem +fun EntityActionStrip( + delegate: HubScreenDelegate, + item: HubItem ) { - Row(Modifier.padding(horizontal = 16.dp).padding(bottom = 4.dp)) { - IconButton(onClick = { delegate.toggleMainObjectAddedState() }, Modifier.offset(y = 2.dp).align(Alignment.CenterVertically).size(28.dp)) { - Icon(if (delegate.getMainObjectAddedState().value) Icons.Rounded.Favorite else Icons.Rounded.FavoriteBorder, null) - } + Row( + Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 4.dp)) { + IconButton( + onClick = { delegate.toggleMainObjectAddedState() }, + Modifier + .offset(y = 2.dp) + .align(Alignment.CenterVertically) + .size(28.dp) + ) { + Icon( + if (delegate.getMainObjectAddedState().value) Icons.Rounded.Favorite else Icons.Rounded.FavoriteBorder, + null + ) + } - Spacer(Modifier.width(16.dp)) + Spacer(Modifier.width(16.dp)) - IconButton(onClick = { /*TODO*/ }, Modifier.offset(y = 2.dp).align(Alignment.CenterVertically).size(28.dp)) { - Icon(Icons.Rounded.MoreVert, null) - } + IconButton( + onClick = { /*TODO*/ }, + Modifier + .offset(y = 2.dp) + .align(Alignment.CenterVertically) + .size(28.dp) + ) { + Icon(Icons.Rounded.MoreVert, null) + } - Spacer(Modifier.weight(1f)) + Spacer(Modifier.weight(1f)) - Box(Modifier.size(48.dp)) { - Box( - Modifier.clip(CircleShape).size(48.dp).background(MaterialTheme.colorScheme.primary).clickableHub(item.children!![0]) - ) { - Icon( - imageVector = Icons.Rounded.PlayArrow, - tint = MaterialTheme.colorScheme.onPrimary, - contentDescription = null, - modifier = Modifier.size(32.dp).align(Alignment.Center) - ) - } + Box(Modifier.size(48.dp)) { + Box( + Modifier + .clip(CircleShape) + .size(48.dp) + .background(MaterialTheme.colorScheme.primary) + .clickableHub(item.children!![0]) + ) { + Icon( + imageVector = Icons.Rounded.PlayArrow, + tint = MaterialTheme.colorScheme.onPrimary, + contentDescription = null, + modifier = Modifier + .size(32.dp) + .align(Alignment.Center) + ) + } - if ((item.children[0].events?.click as? HubEvent.PlayFromContext)?.data?.player?.options?.player_options_override?.shuffling_context != false) { - Box( - Modifier.align(Alignment.BottomEnd).offset(4.dp, 4.dp).clip(CircleShape).size(22.dp) - .background(MaterialTheme.colorScheme.compositeSurfaceElevation(4.dp)) - ) { - Icon( - imageVector = Icons.Rounded.Shuffle, - tint = MaterialTheme.colorScheme.primary, - contentDescription = null, - modifier = Modifier.padding(4.dp).align(Alignment.Center) - ) + if ((item.children[0].events?.click as? HubEvent.PlayFromContext)?.data?.player?.options?.player_options_override?.shuffling_context != false) { + Box( + Modifier + .align(Alignment.BottomEnd) + .offset(4.dp, 4.dp) + .clip(CircleShape) + .size(22.dp) + .background(MaterialTheme.colorScheme.compositeSurfaceElevation(4.dp)) + ) { + Icon( + imageVector = Icons.Rounded.Shuffle, + tint = MaterialTheme.colorScheme.primary, + contentDescription = null, + modifier = Modifier + .padding(4.dp) + .align(Alignment.Center) + ) + } + } } - } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/virt/PlaylistEntityView.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/virt/PlaylistEntityView.kt index 7ad996d5..c455dada 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/virt/PlaylistEntityView.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/virt/PlaylistEntityView.kt @@ -8,6 +8,7 @@ import bruhcollective.itaysonlab.jetispot.core.objs.hub.* import bruhcollective.itaysonlab.jetispot.core.objs.player.* import bruhcollective.itaysonlab.jetispot.core.tracks import bruhcollective.itaysonlab.jetispot.core.user +import bruhcollective.itaysonlab.jetispot.ui.screens.hub.LikedSongsViewModel import com.google.protobuf.ByteString import com.google.protobuf.StringValue import com.spotify.metadata.Metadata diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/Screen.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/Screen.kt index af95998d..3bd524a8 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/Screen.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/Screen.kt @@ -28,6 +28,7 @@ enum class Screen( // config Config("config"), StorageConfig("config/storage"), + LanguageConfig("config/language"), QualityConfig("config/playbackQuality"), NormalizationConfig("config/playbackNormalization"); @@ -47,12 +48,14 @@ enum class Dialog( val route: String ) { AuthDisclaimer("dialogs/disclaimers"), - Logout("dialogs/logout") + Logout("dialogs/logout"), + UpdateAvailable("dialogs/updateAvailable") } @Immutable enum class BottomSheet( val route: String ) { - JumpToArtist("bs/jumpToArtist/{artistIdsAndRoles}") // ID=ROLE|ID=ROLE + JumpToArtist("bs/jumpToArtist/{artistIdsAndRoles}"), // ID=ROLE|ID=ROLE + MoreOptions("bs/moreOptions/{trackName}/{artistName}/{artworkUrl}/{artistsData}") //TODO: ADD ARGUMENTS } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/auth/AuthScreen.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/auth/AuthScreen.kt index 51a328c2..096b0de9 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/auth/AuthScreen.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/auth/AuthScreen.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import bruhcollective.itaysonlab.jetispot.R +import bruhcollective.itaysonlab.jetispot.proto.AudioQuality import bruhcollective.itaysonlab.jetispot.ui.ext.compositeSurfaceElevation import bruhcollective.itaysonlab.jetispot.ui.navigation.LocalNavigationController import bruhcollective.itaysonlab.jetispot.ui.screens.Dialog @@ -68,6 +69,11 @@ fun AuthScreen( } } + fun onLoginSuccess(){ + navController.navigateAndClearStack(Screen.Feed) + viewModel.updateAudioQualityIfPremium(AudioQuality.VERY_HIGH) + } + val autofill = LocalAutofill.current val focusManager = LocalFocusManager.current @@ -173,7 +179,7 @@ fun AuthScreen( viewModel.auth( username = username, password = password, - onSuccess = { navController.navigateAndClearStack(Screen.Feed) }, + onSuccess = { onLoginSuccess() }, onFailure = setSnackbarContent, ) }, diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/auth/AuthScreenViewModel.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/auth/AuthScreenViewModel.kt index 682205e4..0764d1c2 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/auth/AuthScreenViewModel.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/auth/AuthScreenViewModel.kt @@ -8,20 +8,38 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import bruhcollective.itaysonlab.jetispot.R import bruhcollective.itaysonlab.jetispot.core.SpAuthManager +import bruhcollective.itaysonlab.jetispot.core.SpConfigurationManager +import bruhcollective.itaysonlab.jetispot.core.SpSessionManager +import bruhcollective.itaysonlab.jetispot.proto.AppConfig +import bruhcollective.itaysonlab.jetispot.proto.AudioQuality +import bruhcollective.itaysonlab.jetispot.ui.screens.config.QualityConfigScreenViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject +import bruhcollective.itaysonlab.jetispot.proto.PlayerConfig as PlayerConfig @Stable @HiltViewModel class AuthScreenViewModel @Inject constructor( private val authManager: SpAuthManager, private val resources: Resources, + private val spSessionManager: SpSessionManager, + private val spConfigurationManager: SpConfigurationManager ) : ViewModel() { private val _isAuthInProgress = mutableStateOf(false) val isAuthInProgress: State = _isAuthInProgress + fun updateAudioQualityIfPremium(audioQuality: AudioQuality) { + viewModelScope.launch { + if (spSessionManager.session?.getUserAttribute("player-license") == "premium") { + modifyDatastore { + playerConfig = playerConfig.toBuilder().setPreferredQuality(audioQuality).build() + } + } + } + } + fun auth( username: String, password: String, @@ -53,4 +71,9 @@ class AuthScreenViewModel @Inject constructor( _isAuthInProgress.value = false } } + suspend fun modifyDatastore(runOnBuilder: AppConfig.Builder.() -> Unit) { + spConfigurationManager.dataStore.updateData { + it.toBuilder().apply(runOnBuilder).build() + } + } } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/config/ConfigScreen.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/config/ConfigScreen.kt index 88d469cb..972540c6 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/config/ConfigScreen.kt @@ -31,6 +31,9 @@ class ConfigScreenViewModel @Inject constructor( private val spConfigurationManager: SpConfigurationManager ) : ViewModel(), ConfigViewModel { private val configList = buildList { + + add(ConfigItem.Hint()) + add(ConfigItem.Category(R.string.config_playback)) add(ConfigItem.Preference(R.string.config_pbquality, { ctx, cfg -> @@ -84,6 +87,10 @@ class ConfigScreenViewModel @Inject constructor( it.navigate(Screen.StorageConfig) })) + /*add(ConfigItem.Preference(R.string.language, { ctx, cfg -> "" }, { + it.navigate(Screen.LanguageConfig) + }))*/ + add(ConfigItem.Category(R.string.config_account)) add(ConfigItem.Preference(R.string.config_logout, { ctx, cfg -> @@ -109,7 +116,7 @@ class ConfigScreenViewModel @Inject constructor( }, {})) add(ConfigItem.Preference(R.string.about_sources, { ctx, _ -> "" }, { - it.openInBrowser("https://github.com/itaysonlab/jetispot") + it.openInBrowser("https://github.com/iTaysonLab/jetispot") })) add(ConfigItem.Preference(R.string.about_channel, { ctx, _ -> "" }, { diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/config/QualityConfigScreen.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/config/QualityConfigScreen.kt index 41e61613..de01adc9 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/config/QualityConfigScreen.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/config/QualityConfigScreen.kt @@ -8,55 +8,69 @@ import bruhcollective.itaysonlab.jetispot.core.SpConfigurationManager import bruhcollective.itaysonlab.jetispot.core.SpSessionManager import bruhcollective.itaysonlab.jetispot.proto.AppConfig import bruhcollective.itaysonlab.jetispot.proto.AudioQuality +import com.spotify.pamviewservice.v1.proto.PremiumPlanRow import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @Composable fun QualityConfigScreen( - viewModel: QualityConfigScreenViewModel = hiltViewModel() + viewModel: QualityConfigScreenViewModel = hiltViewModel() ) { - BaseConfigScreen(viewModel) + BaseConfigScreen(viewModel) } @HiltViewModel class QualityConfigScreenViewModel @Inject constructor( - private val spSessionManager: SpSessionManager, - private val spConfigurationManager: SpConfigurationManager + private val spSessionManager: SpSessionManager, + private val spConfigurationManager: SpConfigurationManager ) : ViewModel(), ConfigViewModel { - private val configList = buildList { - add(ConfigItem.Radio(R.string.quality_low, R.string.quality_low_desc, { - it.playerConfig.preferredQuality == AudioQuality.LOW - }, { true }, { - playerConfig = playerConfig.toBuilder().setPreferredQuality(AudioQuality.LOW).build() - })) - - add(ConfigItem.Radio(R.string.quality_normal, R.string.quality_normal_desc, { - it.playerConfig.preferredQuality == AudioQuality.NORMAL - }, { true }, { - playerConfig = playerConfig.toBuilder().setPreferredQuality(AudioQuality.NORMAL).build() - })) - - add(ConfigItem.Radio(R.string.quality_high, R.string.quality_high_desc, { - it.playerConfig.preferredQuality == AudioQuality.HIGH - }, { true }, { - playerConfig = playerConfig.toBuilder().setPreferredQuality(AudioQuality.HIGH).build() - })) - - add(ConfigItem.Radio(R.string.quality_very_high, R.string.quality_very_high_desc, { - it.playerConfig.preferredQuality == AudioQuality.VERY_HIGH - }, { true }, { - playerConfig = playerConfig.toBuilder().setPreferredQuality(AudioQuality.VERY_HIGH).build() - })) - - add(ConfigItem.Info(R.string.warn_quality)) - } - - override fun provideTitle() = R.string.config_pbquality - override fun provideDataStore() = spConfigurationManager.dataStore - override fun provideConfigList() = configList - override suspend fun modifyDatastore (runOnBuilder: AppConfig.Builder.() -> Unit) { - spConfigurationManager.dataStore.updateData { - it.toBuilder().apply(runOnBuilder).build() + + suspend fun updateAudioQuality(audioQuality: AudioQuality) { + kotlin.run { + modifyDatastore { + playerConfig = playerConfig.toBuilder().setPreferredQuality(audioQuality).build() + } + } + } + + private val configList = buildList { + add(ConfigItem.Radio(R.string.quality_low, R.string.quality_low_desc, { + it.playerConfig.preferredQuality == AudioQuality.LOW + }, { true }, { + playerConfig = playerConfig.toBuilder().setPreferredQuality(AudioQuality.LOW).build() + })) + + add(ConfigItem.Radio(R.string.quality_normal, R.string.quality_normal_desc, { + it.playerConfig.preferredQuality == AudioQuality.NORMAL + }, { true }, { + playerConfig = playerConfig.toBuilder().setPreferredQuality(AudioQuality.NORMAL).build() + })) + + add(ConfigItem.Radio(R.string.quality_high, R.string.quality_high_desc, { + it.playerConfig.preferredQuality == AudioQuality.HIGH + }, { true }, { + playerConfig = playerConfig.toBuilder().setPreferredQuality(AudioQuality.HIGH).build() + })) + + //if the user doesn't have premium, don't show the option to select very high quality + if (spSessionManager.session?.getUserAttribute("name") != "Spotify Free") { + add(ConfigItem.Radio(R.string.quality_very_high, R.string.quality_very_high_desc, { + it.playerConfig.preferredQuality == AudioQuality.VERY_HIGH + }, { true }, { + playerConfig = + playerConfig.toBuilder().setPreferredQuality(AudioQuality.VERY_HIGH).build() + })) + } + + add(ConfigItem.Info(R.string.warn_quality)) + } + + override fun provideTitle() = R.string.config_pbquality + override fun provideDataStore() = spConfigurationManager.dataStore + override fun provideConfigList() = configList + override suspend fun modifyDatastore(runOnBuilder: AppConfig.Builder.() -> Unit) { + spConfigurationManager.dataStore.updateData { + it.toBuilder().apply(runOnBuilder).build() + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/config/SharedConfigUi.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/config/SharedConfigUi.kt index 2dcc1545..20a4a2d3 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/config/SharedConfigUi.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/config/SharedConfigUi.kt @@ -1,29 +1,47 @@ package bruhcollective.itaysonlab.jetispot.ui.screens.config import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.PowerManager +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Translate import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.rounded.EnergySavingsLeaf import androidx.compose.material.icons.rounded.Info import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.datastore.core.DataStore +import bruhcollective.itaysonlab.jetispot.R +import bruhcollective.itaysonlab.jetispot.SpApp.Companion.context import bruhcollective.itaysonlab.jetispot.core.SpConfigurationManager import bruhcollective.itaysonlab.jetispot.proto.AppConfig import bruhcollective.itaysonlab.jetispot.ui.ext.rememberEUCScrollBehavior @@ -51,6 +69,14 @@ fun BaseConfigScreen( val dsConfig = dsConfigState.value val navController = LocalNavigationController.current + //Energy things + val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager + var showBatteryHint by remember { mutableStateOf(!pm.isIgnoringBatteryOptimizations(context.packageName)) } + val launcher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + showBatteryHint = !pm.isIgnoringBatteryOptimizations(context.packageName) + } + Scaffold(topBar = { LargeTopAppBar(title = { Text(stringResource(viewModel.provideTitle())) @@ -68,6 +94,26 @@ fun BaseConfigScreen( .padding(padding)) { items(viewModel.provideConfigList()) { item -> when (item) { + is ConfigItem.Hint -> { + Column(modifier = Modifier.padding()) { + androidx.compose.animation.AnimatedVisibility( + visible = showBatteryHint, + exit = shrinkVertically() + fadeOut() + ) { + PreferencesHint( + title = stringResource(R.string.battery_configuration), + icon = Icons.Rounded.EnergySavingsLeaf, + description = stringResource(R.string.battery_configuration_desc) + ) { + launcher.launch(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:${context.packageName}") + }) + showBatteryHint = !pm.isIgnoringBatteryOptimizations(context.packageName) + } + } + } + } + is ConfigItem.Category -> { ConfigCategory(stringResource(item.title)) } @@ -314,6 +360,59 @@ fun ConfigSlider( } } +@Composable +fun PreferencesHint( + title: String = "Title ".repeat(2), + description: String? = "Description text ".repeat(3), + icon: ImageVector? = Icons.Outlined.Translate, + onClick: () -> Unit = {}, +) { + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + .clip(MaterialTheme.shapes.extraLarge) + .background(MaterialTheme.colorScheme.secondaryContainer) + .clickable { onClick() } + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + icon?.let { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier + .padding(start = 8.dp, end = 16.dp) + .size(24.dp), + tint = MaterialTheme.colorScheme.secondary + ) + } + Column( + modifier = Modifier + .weight(1f) + .padding(start = if (icon == null) 12.dp else 0.dp, end = 12.dp) + ) { + with(MaterialTheme) { + + Text( + text = title, + maxLines = 1, + style = typography.titleLarge.copy(fontSize = 20.sp), + color = colorScheme.onSecondaryContainer + ) + if (description != null) + Text( + text = description, + color = colorScheme.onSecondaryContainer, + maxLines = 2, overflow = TextOverflow.Ellipsis, + style = typography.bodyMedium, + ) + } + } + } +} + // sealed class ConfigItem { @@ -355,4 +454,7 @@ sealed class ConfigItem { val state: (AppConfig) -> Int, val modify: AppConfig.Builder.(Int) -> Unit ) : ConfigItem() + + class Hint( + ) : ConfigItem() } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/history/ListeningHistoryScreen.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/history/ListeningHistoryScreen.kt index db3de392..9a1590d6 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/history/ListeningHistoryScreen.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/history/ListeningHistoryScreen.kt @@ -41,12 +41,13 @@ class HistoryViewModel @Inject constructor( private val spInternalApi: SpInternalApi, private val spPlayerServiceManager: SpPlayerServiceManager ) : AbsHubViewModel(), HubScreenDelegate { + suspend fun load() = load { - spInternalApi.getListeningHistory() + spInternalApi.getListeningHistory() } suspend fun reload() = reload { - spInternalApi.getListeningHistory() + spInternalApi.getListeningHistory() } override fun play(data: PlayFromContextData) { diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/HubExt.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/HubExt.kt index 9d42224b..773c1f1e 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/HubExt.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/HubExt.kt @@ -29,80 +29,110 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun HubScaffold( - appBarTitle: String, - state: HubState, - viewModel: HubScreenDelegate, - toolbarOptions: ToolbarOptions = ToolbarOptions(), - reloadFunc: suspend () -> Unit + appBarTitle: String, + state: HubState, + viewModel: HubScreenDelegate, + toolbarOptions: ToolbarOptions = ToolbarOptions(), + reloadFunc: suspend () -> Unit ) { - val navController = LocalNavigationController.current - val scope = rememberCoroutineScope() - val scrollBehavior = if (toolbarOptions.alwaysVisible) TopAppBarDefaults.exitUntilCollapsedScrollBehavior() else TopAppBarDefaults.pinnedScrollBehavior() + val navController = LocalNavigationController.current + val scope = rememberCoroutineScope() + val scrollBehavior = + if (toolbarOptions.alwaysVisible) TopAppBarDefaults.exitUntilCollapsedScrollBehavior() else TopAppBarDefaults.pinnedScrollBehavior() - when (state) { - is HubState.Loaded -> { - Scaffold(topBar = { - if (toolbarOptions.big) { - LargeTopAppBar(title = { - Text(appBarTitle, maxLines = 1, overflow = TextOverflow.Ellipsis) - }, navigationIcon = { - IconButton(onClick = { navController.popBackStack() }) { - Icon(Icons.Rounded.ArrowBack, null) - } - }, colors = TopAppBarDefaults.largeTopAppBarColors(), scrollBehavior = scrollBehavior) - } else { - TopAppBar(title = { - Text(appBarTitle, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.alpha(scrollBehavior.state.overlappedFraction)) - }, navigationIcon = { - IconButton(onClick = { navController.popBackStack() }) { - Icon(Icons.Rounded.ArrowBack, null) - } - }, colors = if (toolbarOptions.alwaysVisible) TopAppBarDefaults.smallTopAppBarColors() else TopAppBarDefaults.smallTopAppBarColors( - containerColor = Color.Transparent, - scrolledContainerColor = MaterialTheme.colorScheme.compositeSurfaceElevation(3.dp) - ), scrollBehavior = scrollBehavior) - } - }, modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), contentWindowInsets = WindowInsets(top = 0.dp)) { padding -> - CompositionLocalProvider(LocalHubScreenDelegate provides viewModel) { - LazyColumn( - modifier = Modifier - .fillMaxHeight() - .let { if (toolbarOptions.alwaysVisible) it.padding(padding) else it } - ) { - state.data.apply { - if (header != null) { - item( - key = header.id, - contentType = header.component.javaClass.simpleName, - ) { - HubBinder(header) - } - } + when (state) { + is HubState.Loaded -> { + Scaffold( + topBar = { + if (toolbarOptions.big) { + LargeTopAppBar( + title = { + Text(appBarTitle, maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon(Icons.Rounded.ArrowBack, null) + } + }, + colors = TopAppBarDefaults.largeTopAppBarColors(), + scrollBehavior = scrollBehavior + ) + } else { + TopAppBar( + title = { + Text( + appBarTitle, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.alpha(scrollBehavior.state.overlappedFraction) + ) + }, + navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon(Icons.Rounded.ArrowBack, null) + } + }, + colors = if (toolbarOptions.alwaysVisible) TopAppBarDefaults.smallTopAppBarColors() else TopAppBarDefaults.smallTopAppBarColors( + containerColor = Color.Transparent, + scrolledContainerColor = MaterialTheme.colorScheme.compositeSurfaceElevation( + 3.dp + ) + ), + scrollBehavior = scrollBehavior + ) + } + }, + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + contentWindowInsets = WindowInsets(top = 0.dp) + ) { padding -> + CompositionLocalProvider(LocalHubScreenDelegate provides viewModel) { + LazyColumn( + modifier = Modifier + .fillMaxHeight() + .let { if (toolbarOptions.alwaysVisible) it.padding(padding) else it } + ) { + state.data.apply { + + if (header != null) { + item( + key = header.id, + contentType = header.component.javaClass.simpleName, + ) { + HubBinder(header) + } + } + items( + body, + key = { it.id }, + contentType = { it.component.javaClass.simpleName }) { + Box(modifier = Modifier.animateItemPlacement()) { + HubBinder(it) + } + } - items(body, key = { it.id }, contentType = { it.component.javaClass.simpleName }) { - Box(modifier = Modifier.animateItemPlacement()) { - HubBinder(it) + } + } } - } } - } } - } + is HubState.Error -> PagingErrorPage( + exception = state.error, + onReload = { scope.launch { reloadFunc() } }, + modifier = Modifier.fillMaxSize() + ) + HubState.Loading -> PagingLoadingPage(Modifier.fillMaxSize()) } - is HubState.Error -> PagingErrorPage(exception = state.error, onReload = { scope.launch { reloadFunc() } }, modifier = Modifier.fillMaxSize()) - HubState.Loading -> PagingLoadingPage(Modifier.fillMaxSize()) - } } sealed class HubState { - object Loading: HubState() - class Error (val error: Exception): HubState() - class Loaded (val data: HubResponse): HubState() + object Loading : HubState() + class Error(val error: Exception) : HubState() + class Loaded(val data: HubResponse) : HubState() } class ToolbarOptions( - val big: Boolean = false, - val alwaysVisible: Boolean = false + val big: Boolean = false, + val alwaysVisible: Boolean = false ) \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/HubScreen.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/HubScreen.kt index efa697c9..0c164ae5 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/HubScreen.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/HubScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -17,9 +18,12 @@ import bruhcollective.itaysonlab.jetispot.core.collection.SpCollectionManager import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubResponse import bruhcollective.itaysonlab.jetispot.core.objs.hub.isGrid import bruhcollective.itaysonlab.jetispot.core.objs.player.PlayFromContextData +import bruhcollective.itaysonlab.jetispot.core.util.UpdateUtil import bruhcollective.itaysonlab.jetispot.ui.hub.HubBinder import bruhcollective.itaysonlab.jetispot.ui.hub.HubScreenDelegate import bruhcollective.itaysonlab.jetispot.ui.hub.LocalHubScreenDelegate +import bruhcollective.itaysonlab.jetispot.ui.navigation.LocalNavigationController +import bruhcollective.itaysonlab.jetispot.ui.screens.Dialog import bruhcollective.itaysonlab.jetispot.ui.shared.PagingErrorPage import bruhcollective.itaysonlab.jetispot.ui.shared.PagingLoadingPage import dagger.hilt.android.lifecycle.HiltViewModel @@ -35,6 +39,10 @@ fun HubScreen( onAppBarTitleChange: (String) -> Unit = {}, ) { val scope = rememberCoroutineScope() + var latestRelease by remember { mutableStateOf(UpdateUtil.LatestRelease()) } + var showUpdateDialog by rememberSaveable { mutableStateOf(false) } + + val navController = LocalNavigationController.current viewModel.needContentPadding = needContentPadding @@ -42,6 +50,10 @@ fun HubScreen( viewModel.load(onAppBarTitleChange, loader) } + if(showUpdateDialog) { + navController.navigate(Dialog.UpdateAvailable) + } + when (viewModel.state) { is HubScreenViewModel.State.Loaded -> { CompositionLocalProvider(LocalHubScreenDelegate provides viewModel) { diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/LikedSongsScreen.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/LikedSongsScreen.kt index 46d4d0ef..23173954 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/LikedSongsScreen.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/LikedSongsScreen.kt @@ -1,5 +1,6 @@ package bruhcollective.itaysonlab.jetispot.ui.screens.hub +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.graphics.Color @@ -41,6 +42,7 @@ class LikedSongsViewModel @Inject constructor( override suspend fun calculateDominantColor(url: String, dark: Boolean) = Color.Transparent suspend fun load(fullUri: String, id: String) = load { + Log.i("LikedSongsViewModel", "Loading liked songs; Full URI: $fullUri, ID: $id") val artistTracks = spCollectionManager.tracksByArtist(ArtistId.fromBase62(id).hexId()) HubResponse(body = buildList { diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/PodcastShowScreen.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/PodcastShowScreen.kt index b3bb048c..0d93ddd8 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/PodcastShowScreen.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/PodcastShowScreen.kt @@ -1,5 +1,6 @@ package bruhcollective.itaysonlab.jetispot.ui.screens.hub +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf @@ -15,33 +16,37 @@ import javax.inject.Inject @Composable fun PodcastShowScreen( - id: String, - viewModel: PodcastShowViewModel = hiltViewModel() + id: String, + viewModel: PodcastShowViewModel = hiltViewModel() ) { - LaunchedEffect(Unit) { - viewModel.load { viewModel.loadInternal(id) } - } + LaunchedEffect(Unit) { + Log.d("PodcastShowScreen", "LaunchedEffect - Podcast Id: $id") + viewModel.load { viewModel.loadInternal(id) } + } - HubScaffold( - appBarTitle = viewModel.title.value, - state = viewModel.state, - viewModel = viewModel - ) { - viewModel.reload { viewModel.loadInternal(id) } - } + HubScaffold( + appBarTitle = viewModel.title.value, + state = viewModel.state, + viewModel = viewModel + ) { + viewModel.reload { viewModel.loadInternal(id) } + } } @HiltViewModel class PodcastShowViewModel @Inject constructor( - private val spSessionManager: SpSessionManager, - private val spPartnersApi: SpPartnersApi, - private val spPlayerServiceManager: SpPlayerServiceManager, - private val spMetadataRequester: SpMetadataRequester + private val spSessionManager: SpSessionManager, + private val spPartnersApi: SpPartnersApi, + private val spPlayerServiceManager: SpPlayerServiceManager, + private val spMetadataRequester: SpMetadataRequester ) : AbsHubViewModel() { - val title = mutableStateOf("") + val title = mutableStateOf("") - suspend fun loadInternal(id: String) = ShowEntityView.create(spSessionManager, spMetadataRequester, id).also { title.value = it.title ?: "" } + suspend fun loadInternal(id: String) = + ShowEntityView.create(spSessionManager, spMetadataRequester, id) + .also { title.value = it.title ?: "" } - override fun play(data: PlayFromContextData) = play(spPlayerServiceManager, data) - override suspend fun calculateDominantColor(url: String, dark: Boolean) = calculateDominantColor(spPartnersApi, url, dark) + override fun play(data: PlayFromContextData) = play(spPlayerServiceManager, data) + override suspend fun calculateDominantColor(url: String, dark: Boolean) = + calculateDominantColor(spPartnersApi, url, dark) } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingMiniplayer.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingMiniplayer.kt index 12c6422c..1d19163b 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingMiniplayer.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingMiniplayer.kt @@ -1,7 +1,11 @@ package bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme @@ -11,76 +15,299 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import bruhcollective.itaysonlab.jetispot.R import bruhcollective.itaysonlab.jetispot.core.SpPlayerServiceManager +import bruhcollective.itaysonlab.jetispot.ui.shared.MarqueeText import bruhcollective.itaysonlab.jetispot.ui.shared.PlayPauseButton import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableSyncImage @Composable -fun NowPlayingMiniplayer( - viewModel: NowPlayingViewModel, - modifier: Modifier +fun NowPlayingMiniplayer2( + viewModel: NowPlayingViewModel, + modifier: Modifier, + visible: Boolean, + bsOffset: Float, ) { - Surface(tonalElevation = 8.dp, modifier = modifier) { - Box(Modifier.fillMaxSize()) { - LinearProgressIndicator( - progress = viewModel.currentPosition.value.progressRange, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier - .height(2.dp) - .fillMaxWidth() - ) + AnimatedVisibility(visible = visible, enter = fadeIn(), exit = fadeOut()) { + Surface(tonalElevation = 8.dp, modifier = modifier) { + Box(Modifier.fillMaxSize()) { + Row( + Modifier + .fillMaxHeight(0.98f) + .padding(horizontal = 16.dp) + ) { + PreviewableSyncImage( + viewModel.currentTrack.value.artworkCompose, + placeholderType = "track", + modifier = Modifier + .size(48.dp) + .align(Alignment.CenterVertically) + .clip(RoundedCornerShape(8.dp)) + ) + Column( + modifier = Modifier + .align(Alignment.CenterVertically) + .fillMaxWidth() + .padding(start = 16.dp) + ) { + MarqueeText( + if (viewModel.currentTrack.value.title == "Unknown Title") stringResource( + id = R.string.unknown_title + ) else viewModel.currentTrack.value.title, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + MarqueeText( + if (viewModel.currentTrack.value.artist == "Unknown Artist") stringResource( + id = R.string.unknown_artist + ) else viewModel.currentTrack.value.artist, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 12.sp, + ) + } + + } + LinearProgressIndicator( + progress = viewModel.currentPosition.value.progressRange, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .height(2.dp) + .fillMaxWidth() + .align(Alignment.BottomCenter) + ) + } + } + } +} - Row( - Modifier - .fillMaxHeight() - .padding(horizontal = 16.dp) - ) { - PreviewableSyncImage( - viewModel.currentTrack.value.artworkCompose, - placeholderType = "track", - modifier = Modifier - .size(48.dp) - .align(Alignment.CenterVertically) - .clip(RoundedCornerShape(8.dp)) - ) + @Composable + fun NowPlayingMiniplayer( + viewModel: NowPlayingViewModel, + modifier: Modifier, + visible: Boolean, + bsOffset: Float, + ) { + AnimatedVisibility(visible = visible, enter = fadeIn(), exit = fadeOut()) { + Surface(tonalElevation = 8.dp, modifier = modifier) { + Box(Modifier.fillMaxSize()) { + LinearProgressIndicator( + progress = viewModel.currentPosition.value.progressRange, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .height(2.dp) + .fillMaxWidth() + .align(Alignment.BottomCenter) + ) + Row( + Modifier + .fillMaxHeight() + .padding(horizontal = 16.dp) + ) { + PreviewableSyncImage( + viewModel.currentTrack.value.artworkCompose, + placeholderType = "track", + modifier = Modifier + .size(48.dp) + .align(Alignment.CenterVertically) + .clip(RoundedCornerShape(8.dp)) + ) - Column( - Modifier - .weight(2f) - .padding(horizontal = 14.dp) - .align(Alignment.CenterVertically) - ) { - Text( - viewModel.currentTrack.value.title, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontSize = 16.sp - ) - Text( - viewModel.currentTrack.value.artist, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontSize = 12.sp, - modifier = Modifier.padding(top = 2.dp) - ) + Column( + Modifier + .weight(2f) + .padding(horizontal = 14.dp) + .align(Alignment.CenterVertically) + ) { + MarqueeText( + if (viewModel.currentTrack.value.title == "Unknown Title") stringResource( + id = R.string.unknown_title + ) else viewModel.currentTrack.value.title, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + MarqueeText( + if (viewModel.currentTrack.value.artist == "Unknown Artist") stringResource( + id = R.string.unknown_artist + ) else viewModel.currentTrack.value.artist, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 12.sp, + ) + } + Surface( + modifier = Modifier + .width(64.dp) + .fillMaxHeight() + .padding(vertical = 12.dp), + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(0.1f), + shape = CircleShape + ) { + PlayPauseButton( + viewModel.currentState.value == SpPlayerServiceManager.PlaybackState.Playing, + MaterialTheme.colorScheme.onSecondaryContainer.copy(0.85f), + Modifier + .width(56.dp) + .align(Alignment.CenterVertically) + .clickable { viewModel.togglePlayPause() } + ) + } + } + } + } } + } + +/* + Row( + Modifier + .fillMaxHeight(0.98f) + .padding(horizontal = 16.dp) + ) { + PreviewableSyncImage( + viewModel.currentTrack.value.artworkCompose, + placeholderType = "track", + modifier = Modifier + .size(48.dp) + .align(Alignment.CenterVertically) + .clip(RoundedCornerShape(8.dp)) + ) + Column( + modifier = Modifier + .align(Alignment.CenterVertically) + .fillMaxWidth() + .padding(start = 16.dp) + ) { + MarqueeText( + if (viewModel.currentTrack.value.title == "Unknown Title") stringResource( + id = R.string.unknown_title + ) else viewModel.currentTrack.value.title, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + MarqueeText( + if (viewModel.currentTrack.value.artist == "Unknown Artist") stringResource( + id = R.string.unknown_artist + ) else viewModel.currentTrack.value.artist, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 12.sp, + ) + } + Row( + modifier = Modifier.align(Alignment.CenterVertically), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Surface( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f), + shape = CircleShape, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 16.dp) + .size(32.dp), + ) { + PlayPauseButton( + viewModel.currentState.value == SpPlayerServiceManager.PlaybackState.Playing, + MaterialTheme.colorScheme.onSecondaryContainer.copy(0.85f), + Modifier + .width(56.dp) + .align(Alignment.CenterVertically) + .clickable { viewModel.togglePlayPause() } + ) + } + } + } + */ - PlayPauseButton( - viewModel.currentState.value == SpPlayerServiceManager.PlaybackState.Playing, - MaterialTheme.colorScheme.onSurface, - Modifier - .fillMaxHeight() - .width(56.dp) - .align(Alignment.CenterVertically).clickable { - viewModel.togglePlayPause() +// ------------------------------------------------------------------------------------ + +/* ORIGINAL +@Composable +fun NowPlayingMiniplayer( + viewModel: NowPlayingViewModel, + modifier: Modifier, + visible: Boolean +) { + AnimatedVisibility(visible = visible, enter = fadeIn(), exit = fadeOut()) { + Surface(tonalElevation = 8.dp, modifier = modifier) { + Box(Modifier.fillMaxSize()) { + LinearProgressIndicator( + progress = viewModel.currentPosition.value.progressRange, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .height(2.dp) + .fillMaxWidth() + ) + + Row( + Modifier + .fillMaxHeight() + .padding(horizontal = 16.dp) + ) { + PreviewableSyncImage( + viewModel.currentTrack.value.artworkCompose, + placeholderType = "track", + modifier = Modifier + .size(48.dp) + .align(Alignment.CenterVertically) + .clip(RoundedCornerShape(8.dp)) + ) + + Column( + Modifier + .weight(2f) + .padding(horizontal = 14.dp) + .align(Alignment.CenterVertically) + ) { + Text( + if (viewModel.currentTrack.value.title == "Unknown Title") stringResource(id = R.string.unknown_title) else viewModel.currentTrack.value.title, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 16.sp + ) + Text( + if(viewModel.currentTrack.value.artist == "Unknown Artist") stringResource(id = R.string.unknown_artist) else viewModel.currentTrack.value.artist, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 12.sp, + modifier = Modifier.padding(top = 2.dp) + ) + } + + PlayPauseButton( + viewModel.currentState.value == SpPlayerServiceManager.PlaybackState.Playing, + MaterialTheme.colorScheme.onSurface, + Modifier + .fillMaxHeight() + .width(56.dp) + .align(Alignment.CenterVertically) + .clickable { + viewModel.togglePlayPause() + } + ) + } } - ) - } + } } - } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingScreen.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingScreen.kt index e9fcfcba..3e5261b6 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingScreen.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingScreen.kt @@ -1,10 +1,13 @@ package bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.BottomSheetState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.Composable @@ -12,6 +15,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying.fullscreen.NowPlayingFullscreenComposition @@ -20,34 +25,41 @@ import kotlinx.coroutines.launch @Composable @OptIn(ExperimentalMaterialApi::class) fun NowPlayingScreen( - bottomSheetState: BottomSheetState, - bsOffset: () -> Float, - queueOpened: Boolean, - setQueueOpened: (Boolean) -> Unit, - lyricsOpened: Boolean, - setLyricsOpened: (Boolean) -> Unit, - viewModel: NowPlayingViewModel = hiltViewModel() + bottomSheetState: BottomSheetState, + bsOffset: () -> Float, + queueOpened: Boolean, + setQueueOpened: (Boolean) -> Unit, + lyricsOpened: Boolean, + setLyricsOpened: (Boolean) -> Unit, + viewModel: NowPlayingViewModel = hiltViewModel() ) { - val scope = rememberCoroutineScope() + val scope = rememberCoroutineScope() - Box(Modifier.fillMaxSize()) { - NowPlayingFullscreenComposition( - queueOpened = queueOpened, - setQueueOpened = setQueueOpened, - lyricsOpened = lyricsOpened, - setLyricsOpened = setLyricsOpened, - bottomSheetState = bottomSheetState, - viewModel = viewModel - ) + Box( + Modifier + .fillMaxSize() + .background(Color.Transparent) + ) { + NowPlayingFullscreenComposition( + queueOpened = queueOpened, + setQueueOpened = setQueueOpened, + lyricsOpened = lyricsOpened, + setLyricsOpened = setLyricsOpened, + bottomSheetState = bottomSheetState, + viewModel = viewModel, + bsOffset = bsOffset() + ) - NowPlayingMiniplayer( - viewModel, - Modifier - .alpha(1f - bsOffset()) - .clickable { scope.launch { bottomSheetState.expand() } } - .fillMaxWidth() - .height(72.dp) - .align(Alignment.TopStart) - ) - } + NowPlayingMiniplayer( + viewModel, + Modifier + .alpha(1f - bsOffset()) + .clickable { scope.launch { bottomSheetState.expand() } } + .fillMaxWidth() + .height(72.dp) + .align(Alignment.TopStart), + visible = bsOffset() <= 0.99f, + bsOffset = bsOffset() + ) + } } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingViewModel.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingViewModel.kt index 7c2f5886..1d4bd566 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingViewModel.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingViewModel.kt @@ -11,9 +11,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.IntSize import androidx.lifecycle.ViewModel import bruhcollective.itaysonlab.jetispot.R +import bruhcollective.itaysonlab.jetispot.SpApp import bruhcollective.itaysonlab.jetispot.core.SpPlayerServiceManager import bruhcollective.itaysonlab.jetispot.core.api.SpPartnersApi import bruhcollective.itaysonlab.jetispot.core.lyrics.SpLyricsController +import bruhcollective.itaysonlab.jetispot.core.util.Log import bruhcollective.itaysonlab.jetispot.core.util.SpUtils import bruhcollective.itaysonlab.jetispot.ui.ext.blendWith import bruhcollective.itaysonlab.jetispot.ui.navigation.NavigationController @@ -65,6 +67,16 @@ class NowPlayingViewModel @Inject constructor( navigationController.navigate(currentContextUri.value) } + @OptIn(ExperimentalMaterialApi::class) + fun navigateToMoreOptions(navigationController: NavigationController, scope: CoroutineScope, bottomSheetState: BottomSheetState) { + navigationController.navigate(BottomSheet.MoreOptions, mapOf( + "trackName" to currentTrack.value.title, + "artistName" to currentTrack.value.artist, + "artworkUrl" to Utils.bytesToHex(getCurrentTrackAsMetadata().album.coverGroup.imageList[0].fileId).lowercase(), // <-- INFO: THIS IS NOT THE FULL URL. Just the ID of it. Passing a full URL will cause a crash because we can't use them in a DeepLink Navigation URL. + "artistsData" to getCurrentTrackAsMetadata().artistWithRoleList.joinToString("|") { Utils.bytesToHex(it.toByteString()) } + )) + } + @OptIn(ExperimentalMaterialApi::class) fun navigateToArtist(scope: CoroutineScope, sheetState: BottomSheetState, navigationController: NavigationController) { scope.launch { sheetState.collapse() } @@ -113,13 +125,14 @@ class NowPlayingViewModel @Inject constructor( "playlist" -> R.string.playing_src_playlist "album" -> R.string.playing_src_album "artist" -> R.string.playing_src_artist + "search" -> R.string.playing_src_search else -> R.string.playing_src_unknown } } fun getHeaderText(): String { return when { - currentContextUri.value.contains("collection") -> "Liked Songs" // TODO: to R.string + currentContextUri.value.contains("collection") -> SpApp.context.getString(R.string.liked_songs) else -> currentContext.value } } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingBackground.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingBackground.kt index 3e170a52..b0c1be7e 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingBackground.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingBackground.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -38,16 +39,16 @@ fun NowPlayingBackground( ) { val currentColor = viewModel.currentBgColor.value val dominantColorAsBg = animateColorAsState(if (currentColor == Color.Transparent) MaterialTheme.colorScheme.surface else currentColor) - + val isSystemInDarkTheme = isSystemInDarkTheme() Canvas(modifier) { drawRect( brush = Brush.radialGradient( - colors = listOf(dominantColorAsBg.value, Color.Black), + colors = listOf(dominantColorAsBg.value, if(isSystemInDarkTheme) Color.Black else Color.White), center = Offset( - x = size.width * 0.1f, - y = size.height * 0.75f + x = size.width * 0.2f, + y = size.height * 0.55f ), - radius = size.width * 1.5f + radius = size.width * 1.3f ) ) } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingControls.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingControls.kt index 6cffa79d..85fbf43c 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingControls.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingControls.kt @@ -3,6 +3,7 @@ package bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying.fullscreen import android.text.format.DateUtils import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -15,21 +16,19 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import bruhcollective.itaysonlab.jetispot.core.SpPlayerServiceManager import bruhcollective.itaysonlab.jetispot.core.util.SpUtils import bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying.NowPlayingViewModel -import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText -import bruhcollective.itaysonlab.jetispot.ui.shared.PlayPauseButton -import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage -import bruhcollective.itaysonlab.jetispot.ui.shared.navClickable +import bruhcollective.itaysonlab.jetispot.ui.shared.* import com.spotify.metadata.Metadata import kotlinx.coroutines.CoroutineScope @@ -62,20 +61,23 @@ fun NowPlayingControls( private fun ControlsArtwork( viewModel: NowPlayingViewModel, ) { - PreviewableAsyncImage( - imageUrl = remember(viewModel.currentTrack.value) { - if (viewModel.currentQueue.value.isNotEmpty()) { - SpUtils.getImageUrl(viewModel.currentQueue.value[viewModel.currentQueuePosition.value].album.coverGroup.imageList.find { it.size == Metadata.Image.Size.LARGE }?.fileId) - } else { - null - } - }, - placeholderType = "track", - modifier = Modifier - .padding(horizontal = 14.dp) - .size(128.dp) - .clip(RoundedCornerShape(12.dp)) - ) + ElevatedCard(modifier = Modifier + .padding(horizontal = 14.dp) + .clip(RoundedCornerShape(12.dp))) { + PreviewableAsyncImage( + imageUrl = remember(viewModel.currentTrack.value) { + if (viewModel.currentQueue.value.isNotEmpty()) { + SpUtils.getImageUrl(viewModel.currentQueue.value[viewModel.currentQueuePosition.value].album.coverGroup.imageList.find { it.size == Metadata.Image.Size.LARGE }?.fileId) + } else { + null + } + }, + placeholderType = "track", + modifier = Modifier + .size(128.dp) + + ) + } } @OptIn(ExperimentalMaterialApi::class) @@ -85,7 +87,7 @@ private fun ControlsHeader( bottomSheetState: BottomSheetState, viewModel: NowPlayingViewModel, ) { - MediumText( + MarqueeText( text = viewModel.currentTrack.value.title, modifier = Modifier .padding(horizontal = 14.dp) @@ -94,13 +96,15 @@ private fun ControlsHeader( ) { navController -> viewModel.navigateToSource(scope, bottomSheetState, navController) }, - fontSize = 24.sp, color = Color.White, + fontSize = 24.sp, + fontWeight = FontWeight.Bold, ) Spacer(Modifier.height(2.dp)) - - Text(text = viewModel.currentTrack.value.artist, + + MarqueeText(text = viewModel.currentTrack.value.artist, modifier = Modifier + .alpha(0.7f) .padding(horizontal = 14.dp) .navClickable( enableRipple = false @@ -110,7 +114,6 @@ private fun ControlsHeader( maxLines = 1, overflow = TextOverflow.Ellipsis, fontSize = 16.sp, - color = Color.White.copy(alpha = 0.7f) ) } @@ -136,9 +139,9 @@ private fun ControlsSeekbar( } Slider(value = if (isSeekbarDragging) seekbarDraggingProgress else viewModel.currentPosition.value.progressRange, colors = SliderDefaults.colors( - thumbColor = Color.White, - activeTrackColor = Color.White, - inactiveTrackColor = Color.White.copy(alpha = 0.5f) + thumbColor = oppositeColorOfSystem(alpha = 1f), + activeTrackColor = oppositeColorOfSystem(alpha = 0.7f), + inactiveTrackColor = oppositeColorOfSystem(alpha = 0.35f) ), onValueChange = { isSeekbarDragging = true seekbarDraggingProgress = it @@ -154,14 +157,14 @@ private fun ControlsSeekbar( ) { Text( text = elapsedTime, - color = Color.White.copy(alpha = 0.7f), + modifier = Modifier.alpha(0.7f), fontSize = 12.sp ) Spacer(modifier = Modifier.weight(1f)) Text( text = totalTime, - color = Color.White.copy(alpha = 0.7f), - fontSize = 12.sp + fontSize = 12.sp, + modifier = Modifier.alpha(0.7f) ) } } @@ -173,17 +176,17 @@ private fun ControlsMainButtons( setQueueOpened: (Boolean) -> Unit, ) { Row(Modifier.padding(horizontal = 14.dp)) { - Surface(color = Color.White, modifier = Modifier + Surface(color = oppositeColorOfSystem(if (!isSystemInDarkTheme()) 0.75f else 1f), modifier = Modifier .clip(CircleShape) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(color = Color.Black) + indication = rememberRipple(color = systemThemeColor(alpha = 1f)) ) { viewModel.togglePlayPause() }) { PlayPauseButton( isPlaying = viewModel.currentState.value == SpPlayerServiceManager.PlaybackState.Playing, - color = Color.Black, + color = systemThemeColor(1f), modifier = Modifier .size(42.dp) .align(Alignment.CenterVertically) @@ -197,7 +200,7 @@ private fun ControlsMainButtons( modifier = Modifier .clip(CircleShape) .size(42.dp), - colors = IconButtonDefaults.iconButtonColors(containerColor = Color.White.copy(0.2f), contentColor = Color.White) + colors = IconButtonDefaults.iconButtonColors(containerColor = oppositeColorOfSystem(0.2f), contentColor = oppositeColorOfSystem(1f)) ) { Icon(imageVector = Icons.Rounded.SkipPrevious, contentDescription = null) } @@ -209,7 +212,7 @@ private fun ControlsMainButtons( modifier = Modifier .clip(CircleShape) .size(42.dp), - colors = IconButtonDefaults.iconButtonColors(containerColor = Color.White.copy(0.2f), contentColor = Color.White) + colors = IconButtonDefaults.iconButtonColors(containerColor = oppositeColorOfSystem(0.2f), contentColor = oppositeColorOfSystem(1f)) ) { Icon(imageVector = Icons.Rounded.SkipNext, contentDescription = null) } @@ -221,7 +224,7 @@ private fun ControlsMainButtons( modifier = Modifier .clip(CircleShape) .size(42.dp), - colors = IconButtonDefaults.iconButtonColors(containerColor = Color.White.copy(0.2f), contentColor = Color.White) + colors = IconButtonDefaults.iconButtonColors(containerColor = oppositeColorOfSystem(0.2f), contentColor = oppositeColorOfSystem(1f)) ) { Icon(imageVector = Icons.Rounded.FavoriteBorder, contentDescription = null) } @@ -236,9 +239,10 @@ private fun ControlsMainButtons( .onGloballyPositioned { coords -> viewModel.queueButtonParams = coords.positionInRoot() }, - colors = IconButtonDefaults.iconButtonColors(containerColor = Color.Transparent, contentColor = Color.White) + colors = IconButtonDefaults.iconButtonColors(containerColor = Color.Transparent, contentColor = oppositeColorOfSystem(1f)) ) { Icon(imageVector = Icons.Rounded.QueueMusic, contentDescription = null) } } -} \ No newline at end of file +} + diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingFullscreenComposition.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingFullscreenComposition.kt index 12480b47..2a0f2285 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingFullscreenComposition.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingFullscreenComposition.kt @@ -1,10 +1,15 @@ package bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying.fullscreen +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.BottomSheetState import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -14,91 +19,123 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import bruhcollective.itaysonlab.jetispot.ui.ext.compositeSurfaceElevation import bruhcollective.itaysonlab.jetispot.ui.ext.disableTouch import bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying.NowPlayingViewModel +import bruhcollective.itaysonlab.jetispot.ui.theme.ApplicationTheme import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @Composable -fun NowPlayingFullscreenComposition ( - queueOpened: Boolean, - setQueueOpened: (Boolean) -> Unit, - lyricsOpened: Boolean, - setLyricsOpened: (Boolean) -> Unit, - bottomSheetState: BottomSheetState, - viewModel: NowPlayingViewModel +fun NowPlayingFullscreenComposition( + queueOpened: Boolean, + setQueueOpened: (Boolean) -> Unit, + lyricsOpened: Boolean, + setLyricsOpened: (Boolean) -> Unit, + bottomSheetState: BottomSheetState, + bsOffset: Float, + viewModel: NowPlayingViewModel ) { - val scope = rememberCoroutineScope() + val scope = rememberCoroutineScope() - val queueProgress = animateFloatAsState(targetValue = if (queueOpened) 1f else 0f, animationSpec = spring(stiffness = 450f)) - val queueProgressValue = queueProgress.value - - val lyricsProgress = animateFloatAsState(targetValue = if (lyricsOpened) 1f else 0f, animationSpec = spring(stiffness = 450f)) - val lyricsProgressValue = lyricsProgress.value - - val anySuperProgress = remember(queueProgressValue, lyricsProgressValue) { - if (queueProgressValue > 0f) { - queueProgressValue - } else { - lyricsProgressValue - } - } + val queueProgress = animateFloatAsState( + targetValue = if (queueOpened) 1f else 0f, + animationSpec = spring(stiffness = 450f) + ) + val queueProgressValue = queueProgress.value - Box(modifier = Modifier.fillMaxSize()) { - NowPlayingBackground( - viewModel = viewModel, - modifier = Modifier.fillMaxSize(), + val lyricsProgress = animateFloatAsState( + targetValue = if (lyricsOpened) 1f else 0f, + animationSpec = spring(stiffness = 450f) ) + val lyricsProgressValue = lyricsProgress.value - // main content - NowPlayingHeader( - stateTitle = stringResource(id = viewModel.getHeaderTitle()), - onCloseClick = { - if (lyricsOpened) { - setLyricsOpened(false) - } else if (queueOpened) { - setQueueOpened(false) + val anySuperProgress = remember(queueProgressValue, lyricsProgressValue) { + if (queueProgressValue > 0f) { + queueProgressValue } else { - scope.launch { bottomSheetState.collapse() } + lyricsProgressValue } - }, - state = viewModel.getHeaderText(), - queueStateProgress = anySuperProgress, - modifier = Modifier - .statusBarsPadding() - .align(Alignment.TopCenter) - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) + } + Box(modifier = Modifier + .fillMaxSize() + .background( + MaterialTheme.colorScheme.compositeSurfaceElevation( + 3.dp + ) + )) { - // composite + NowPlayingBackground( + viewModel = viewModel, + modifier = Modifier + .fillMaxSize() + .alpha(1f * bsOffset), + ) - if (anySuperProgress != 1f) { - NowPlayingControls( - scope = scope, viewModel = viewModel, bottomSheetState = bottomSheetState, queueOpened = queueOpened, setQueueOpened = setQueueOpened, lyricsOpened = lyricsOpened, setLyricsOpened = setLyricsOpened, modifier = Modifier - .disableTouch(disabled = queueOpened) - .alpha(1f - anySuperProgress) - .align(Alignment.BottomStart) - .fillMaxHeight() - .padding(horizontal = 8.dp) - .padding(bottom = 24.dp) - .navigationBarsPadding() - .offset { - IntOffset(x = 0, y = (-(48).dp.toPx() * (anySuperProgress)).toInt()) - } - ) - } + // main content + AnimatedVisibility(visible = bsOffset >= 0.99f, enter = fadeIn(), exit = fadeOut()) { + NowPlayingHeader( + stateTitle = stringResource(id = viewModel.getHeaderTitle()), + onCloseClick = { + if (lyricsOpened) { + setLyricsOpened(false) + } else if (queueOpened) { + setQueueOpened(false) + } else { + scope.launch { bottomSheetState.collapse() } + } + }, + state = viewModel.getHeaderText(), + queueStateProgress = anySuperProgress, + modifier = Modifier + .statusBarsPadding() + .align(Alignment.TopCenter) + .fillMaxWidth() + .padding(horizontal = 16.dp), + viewModel = viewModel, + bottomSheetState = bottomSheetState, + scope = scope, + ) + } + // composite - NowPlayingQueue( - viewModel = viewModel, - modifier = Modifier.fillMaxSize().alpha(1f - lyricsProgressValue), - rvStateProgress = queueProgressValue - ) + if (anySuperProgress != 1f) { + NowPlayingControls( + scope = scope, + viewModel = viewModel, + bottomSheetState = bottomSheetState, + queueOpened = queueOpened, + setQueueOpened = setQueueOpened, + lyricsOpened = lyricsOpened, + setLyricsOpened = setLyricsOpened, + modifier = Modifier + .disableTouch(disabled = queueOpened) + .alpha(1f - anySuperProgress) + .align(Alignment.BottomStart) + .fillMaxHeight() + .padding(horizontal = 8.dp) + .padding(bottom = 24.dp) + .navigationBarsPadding() + .offset { + IntOffset(x = 0, y = (-(48).dp.toPx() * (anySuperProgress)).toInt()) + } + ) + } - NowPlayingLyricsComposition( - viewModel = viewModel, - modifier = Modifier.fillMaxSize().alpha(1f - queueProgressValue), - rvStateProgress = lyricsProgressValue - ) - } + NowPlayingQueue( + viewModel = viewModel, + modifier = Modifier + .fillMaxSize() + .alpha(1f - lyricsProgressValue), + rvStateProgress = queueProgressValue + ) + + NowPlayingLyricsComposition( + viewModel = viewModel, + modifier = Modifier + .fillMaxSize() + .alpha(1f - queueProgressValue), + rvStateProgress = lyricsProgressValue + ) + } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingHeader.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingHeader.kt index 8bd86f8b..c4b7d6a9 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingHeader.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingHeader.kt @@ -1,6 +1,9 @@ package bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying.fullscreen import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.BottomSheetState +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.KeyboardArrowDown @@ -12,6 +15,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight @@ -20,51 +24,84 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying.NowPlayingViewModel +import bruhcollective.itaysonlab.jetispot.ui.shared.navClickable +import kotlinx.coroutines.CoroutineScope +@OptIn(ExperimentalMaterialApi::class) @Composable fun NowPlayingHeader( - stateTitle: String, - state: String, - queueStateProgress: Float, - onCloseClick: () -> Unit, - modifier: Modifier + stateTitle: String, + state: String, + queueStateProgress: Float, + onCloseClick: () -> Unit, + modifier: Modifier, + viewModel: NowPlayingViewModel, + bottomSheetState: BottomSheetState, + scope: CoroutineScope ) { - Row(modifier, verticalAlignment = Alignment.CenterVertically) { - IconButton(onClick = onCloseClick, Modifier.size(32.dp)) { - Icon(imageVector = Icons.Rounded.KeyboardArrowDown, tint = Color.White, contentDescription = null, modifier = Modifier.scale(1f - queueStateProgress).alpha(1f - queueStateProgress)) - Icon(imageVector = Icons.Rounded.Close, tint = Color.White, contentDescription = null, modifier = Modifier.scale(queueStateProgress).alpha(queueStateProgress)) - } + Row(modifier, verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = onCloseClick, Modifier.size(32.dp)) { + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier + .scale(1f - queueStateProgress) + .alpha(1f - queueStateProgress) + ) + Icon( + imageVector = Icons.Rounded.Close, contentDescription = null, modifier = Modifier + .scale(queueStateProgress) + .alpha(queueStateProgress) + ) + } - Column(Modifier.weight(1f).padding(vertical = 8.dp)) { - Text( - text = stateTitle.uppercase(), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - textAlign = TextAlign.Center, - color = Color.White.copy(alpha = 0.7f), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - letterSpacing = 2.sp, - fontSize = 10.sp - ) + Column( + Modifier + .weight(1f) + .padding(vertical = 8.dp) + ) { + Text( + text = stateTitle.uppercase(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + textAlign = TextAlign.Center, + color = oppositeColorOfSystem(alpha = 0.7f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + letterSpacing = 2.sp, + fontSize = 10.sp + ) - Text( - text = state, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - color = Color.White, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold, - fontSize = 14.sp - ) - } + Text( + text = state, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + color = oppositeColorOfSystem(alpha = 1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + fontSize = 14.sp + ) + } - IconButton(onClick = { /*TODO*/ }, Modifier.size(32.dp)) { - Icon(imageVector = Icons.Rounded.MoreVert, tint = Color.White, contentDescription = null) + Column( + Modifier + .size(32.dp) + .navClickable { navController -> + viewModel.navigateToMoreOptions(navigationController = navController, bottomSheetState = bottomSheetState, scope = scope) + }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Rounded.MoreVert, + tint = oppositeColorOfSystem(alpha = 1f), + contentDescription = null + ) + } } - } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingLyricsComposition.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingLyricsComposition.kt index b2bb8940..36af651d 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingLyricsComposition.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingLyricsComposition.kt @@ -27,6 +27,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import bruhcollective.itaysonlab.jetispot.SpApp +import bruhcollective.itaysonlab.jetispot.R +import bruhcollective.itaysonlab.jetispot.core.lyrics.SpLyricsController import bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying.NowPlayingViewModel @Composable @@ -36,6 +39,7 @@ fun NowPlayingLyricsComposition( rvStateProgress: Float ) { Box(modifier) { + val color = oppositeColorOfSystem(alpha = 0.2f) Canvas(modifier = Modifier.fillMaxSize()) { val offset = Offset( x = lerp(viewModel.lyricsCardParams.first.x, 0f, rvStateProgress), @@ -43,14 +47,22 @@ fun NowPlayingLyricsComposition( ) val size = Size( - width = lerp(viewModel.lyricsCardParams.second.width.toFloat(), size.width, rvStateProgress), - height = lerp(viewModel.lyricsCardParams.second.height.toFloat(), size.height, rvStateProgress), + width = lerp( + viewModel.lyricsCardParams.second.width.toFloat(), + size.width, + rvStateProgress + ), + height = lerp( + viewModel.lyricsCardParams.second.height.toFloat(), + size.height, + rvStateProgress + ), ) val radius = androidx.compose.ui.unit.lerp(12.dp, 0.dp, rvStateProgress).toPx() drawRoundRect( - color = Color.White.copy(alpha = 0.2f), + color = color, topLeft = offset, size = size, cornerRadius = CornerRadius(radius, radius) @@ -68,9 +80,32 @@ fun NowPlayingLyricsComposition( IntOffset(x = 0, y = (48.dp.toPx() * (1f - rvStateProgress)).toInt()) }) { - LazyColumn { - items(viewModel.spLyricsController.currentLyricsLines) { line -> - Text(text = line.words, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold) + LazyColumn(modifier = Modifier.padding(12.dp)) { + if (viewModel.spLyricsController.currentLyricsState == SpLyricsController.LyricsState.Unavailable) { + items(listOf(SpApp.context.getString(R.string.no_lyrics))) { item -> + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = item, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } + + } + } else { + items(viewModel.spLyricsController.currentLyricsLines) { line -> + Text( + text = line.words, + color = oppositeColorOfSystem(alpha = 1f), + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(4.dp)) + } } } } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingLyricsContainer.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingLyricsContainer.kt index 54e2318b..d1e07020 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingLyricsContainer.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingLyricsContainer.kt @@ -46,13 +46,27 @@ fun NowPlayingLyricsContainer( Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Rounded.Lyrics, contentDescription = null, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(8.dp)) - Text(text = "Lyrics", color = Color.White.copy(alpha = 0.7f), letterSpacing = 2.sp, fontSize = 13.sp) + Text( + text = "Lyrics", + color = oppositeColorOfSystem(alpha = 0.7f), + letterSpacing = 2.sp, + fontSize = 13.sp + ) Spacer(modifier = Modifier.weight(1f)) - Icon(Icons.Rounded.Fullscreen, contentDescription = null, modifier = Modifier.size(16.dp)) + Icon( + Icons.Rounded.Fullscreen, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) } Spacer(modifier = Modifier.height(4.dp)) - Text(text = viewModel.spLyricsController.currentSongLine, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold) + Text( + text = viewModel.spLyricsController.currentSongLine, + color = oppositeColorOfSystem(alpha = 1f), + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold + ) } } \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingQueue.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingQueue.kt index 35ee294f..fb48173c 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingQueue.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/NowPlayingQueue.kt @@ -32,6 +32,10 @@ fun NowPlayingQueue( rvStateProgress: Float ) { Box(modifier) { + + //return the color using oppositeSystemColor function + val color = oppositeColorOfSystem(alpha = 0.2f) + Canvas(modifier = Modifier.fillMaxSize()) { val offsetPx = 4.dp.toPx() val sizePx = 40.dp.toPx() @@ -49,7 +53,7 @@ fun NowPlayingQueue( val radius = lerp(36.dp, 0.dp, rvStateProgress).toPx() drawRoundRect( - color = Color.White.copy(alpha = 0.2f), + color = color, topLeft = offset, size = size, cornerRadius = CornerRadius(radius, radius) @@ -67,7 +71,12 @@ fun NowPlayingQueue( IntOffset(x = 0, y = (48.dp.toPx() * (1f - rvStateProgress)).toInt()) }) { - LazyColumn(contentPadding = PaddingValues(bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding())) { + LazyColumn( + contentPadding = PaddingValues( + bottom = WindowInsets.navigationBars.asPaddingValues() + .calculateBottomPadding() + ) + ) { items(viewModel.currentQueue.value) { queueItem -> QueueItem(queueItem) } @@ -105,7 +114,7 @@ private fun QueueItem( ) { MediumText(item.name, fontWeight = FontWeight.Normal) Spacer(modifier = Modifier.height(4.dp)) - Subtext(item.artistList.joinToString(", ") { it.name }, maxLines = 1,) + Subtext(item.artistList.joinToString(", ") { it.name }, maxLines = 1) } } } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/PlayerColorUtils.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/PlayerColorUtils.kt new file mode 100644 index 00000000..5098cda2 --- /dev/null +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/fullscreen/PlayerColorUtils.kt @@ -0,0 +1,19 @@ +package bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying.fullscreen + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +//return color black if system theme is light and white if system theme is dark +@Composable +fun oppositeColorOfSystem(alpha : Float): Color { + val isSystemInDarkTheme = isSystemInDarkTheme() + return if (isSystemInDarkTheme) Color.White.copy(alpha = alpha) else Color.Black.copy(alpha = alpha) +} + +//return the color of the system theme +@Composable +fun systemThemeColor(alpha : Float): Color { + val isSystemInDarkTheme = isSystemInDarkTheme() + return if (isSystemInDarkTheme) Color.Black.copy(alpha = alpha) else Color.White.copy(alpha = alpha) +} \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/search/SearchScreen.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/search/SearchScreen.kt index 966f9074..e3fc5363 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/search/SearchScreen.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/search/SearchScreen.kt @@ -15,9 +15,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.focusTarget import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import bruhcollective.itaysonlab.jetispot.R +import bruhcollective.itaysonlab.jetispot.SpApp import bruhcollective.itaysonlab.jetispot.core.api.SpInternalApi import bruhcollective.itaysonlab.jetispot.proto.SearchEntity import bruhcollective.itaysonlab.jetispot.proto.SearchViewResponse @@ -59,7 +62,7 @@ fun SearchScreen( onValueChange = { viewModel.searchQuery = it }, placeholder = { if (viewModel.searchQuery.text.isEmpty()) { - Text(text = "What would you like to listen?") + Text(text = stringResource(id = R.string.search_placeholder)) } }, leadingIcon = { @@ -123,7 +126,11 @@ private fun SearchBinder( onClick: (SearchEntity.EntityCase, String) -> Unit ) { when { - response?.hitsCount == 0 -> PagingInfoPage(title = "Nothing found", text = "Correct your request and try again", modifier = Modifier.fillMaxSize()) + response?.hitsCount == 0 -> PagingInfoPage( + title = SpApp.context.getString(R.string.search_no_results), + text = SpApp.context.getString(R.string.search_no_results_desc), + modifier = Modifier.fillMaxSize() + ) response == null -> PagingLoadingPage(modifier = Modifier.fillMaxSize()) else -> { LazyColumn(Modifier.fillMaxSize()) { @@ -131,23 +138,23 @@ private fun SearchBinder( val text = remember(entity) { when (entity.entityCase) { SearchEntity.EntityCase.TRACK -> { - "Song • " + entity.track.trackArtistsList.joinToString { it.name } + SpApp.context.getString(R.string.song_prefix_with_dot) + entity.track.trackArtistsList.joinToString { it.name } } SearchEntity.EntityCase.PLAYLIST -> { when { - entity.playlist.personalized -> "Playlist • Personalized for you" - entity.playlist.ownedBySpotify -> "Playlist • By Spotify" - else -> "Playlist" + entity.playlist.personalized -> SpApp.context.getString(R.string.playlist_personalized_for_you) + entity.playlist.ownedBySpotify -> SpApp.context.getString(R.string.playlist_owned_by_spotify) + else -> SpApp.context.getString(R.string.playlist_prefix) } } SearchEntity.EntityCase.ALBUM -> { - "Album • " + entity.album.artistNamesList.joinToString() + SpApp.context.getString(R.string.album_with_dot)+ entity.album.artistNamesList.joinToString() } SearchEntity.EntityCase.ARTIST -> { - "Artist" + SpApp.context.getString(R.string.artist) } else -> "" diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/search/SearchViewModel.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/search/SearchViewModel.kt index 339d1dd8..035ab552 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/search/SearchViewModel.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/search/SearchViewModel.kt @@ -7,8 +7,8 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import bruhcollective.itaysonlab.jetispot.core.SpPlayerServiceManager import bruhcollective.itaysonlab.jetispot.core.api.SpInternalApi -import bruhcollective.itaysonlab.jetispot.core.objs.player.PfcContextData -import bruhcollective.itaysonlab.jetispot.core.objs.player.PlayFromContextPlayerData +import bruhcollective.itaysonlab.jetispot.core.objs.player.* +import bruhcollective.itaysonlab.jetispot.core.util.Log import bruhcollective.itaysonlab.jetispot.core.util.playCommand import bruhcollective.itaysonlab.jetispot.proto.SearchViewResponse import dagger.hilt.android.lifecycle.HiltViewModel @@ -21,6 +21,7 @@ class SearchViewModel @Inject constructor( private val spInternalApi: SpInternalApi, private val spPlayerServiceManager: SpPlayerServiceManager ): ViewModel(), CoroutineScope by MainScope() { + var searchQuery by mutableStateOf(TextFieldValue()) var searchResponse by mutableStateOf(null) @@ -34,11 +35,17 @@ class SearchViewModel @Inject constructor( } fun dispatchPlay(uri: String) { + Log.d("Dispatcher", "Dispatching play command for $uri") spPlayerServiceManager.play( - playCommand(uri) { - contextUri = "spotify:search:${searchQuery.text.replace(" ", "+")}" - } - ) + PlayFromContextData( + "spotify:search:${searchQuery.text.replace(" ", "+")}", + PlayFromContextPlayerData( + context = PfcContextData(url = "context://$uri", uri = uri), + state = PfcState(PfcStateOptions(shuffling_context = false)), + options = PfcOptions(player_options_override = PfcStateOptions(shuffling_context = false)) + ) + + )) } fun clear() { diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/yourlibrary2/YourLibraryContainerScreen.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/yourlibrary2/YourLibraryContainerScreen.kt index 377486c4..da0a53f5 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/yourlibrary2/YourLibraryContainerScreen.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/yourlibrary2/YourLibraryContainerScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel import bruhcollective.itaysonlab.jetispot.R +import bruhcollective.itaysonlab.jetispot.core.api.SpInternalApi import bruhcollective.itaysonlab.jetispot.core.collection.db.LocalCollectionDao import bruhcollective.itaysonlab.jetispot.core.collection.db.model2.CollectionEntry import bruhcollective.itaysonlab.jetispot.core.collection.db.model2.PredefCeType @@ -50,7 +51,7 @@ fun YourLibraryContainerScreen( Scaffold(topBar = { Column { TopAppBar(title = { - Text("Your Library") + Text(stringResource(id = R.string.your_library)) }, navigationIcon = { IconButton(onClick = { /* TODO */ }) { Icon(Icons.Rounded.AccountCircle, null) diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/PreviewableAsyncImage.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/PreviewableAsyncImage.kt index 7f1a0bf4..8ed9051b 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/PreviewableAsyncImage.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/PreviewableAsyncImage.kt @@ -25,7 +25,7 @@ fun PreviewableAsyncImage ( placeholderType: String?, modifier: Modifier ) { - if (imageUrl.isNullOrEmpty() || imageUrl == "https://i.scdn.co/image/") { + if (imageUrl.isNullOrEmpty() || imageUrl == "https://i.scdn.co/image/" || imageUrl.startsWith("spotify:mosaic")) { Box(modifier) { ImagePreview(placeholderType, modifier) } diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/SharedTexts.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/SharedTexts.kt index aedd9d1f..f4812af6 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/SharedTexts.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/SharedTexts.kt @@ -1,15 +1,31 @@ package bruhcollective.itaysonlab.jetispot.ui.shared +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay @Composable fun MediumText ( @@ -42,4 +58,145 @@ fun SubtextOverline ( modifier: Modifier = Modifier ) { Text(text, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), letterSpacing = 2.sp, fontSize = 12.sp, lineHeight = 18.sp, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = modifier) -} \ No newline at end of file +} + +@Composable +fun MarqueeText( + text: String, + modifier: Modifier = Modifier, + textModifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + maxLines: Int = 1, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current.plus(TextStyle(platformStyle = PlatformTextStyle(includeFontPadding = false))), + sideGradientColor: Color = Color.Transparent, + basicGradientColor: Color = Color.Transparent, +) { + val createText = @Composable { localModifier: Modifier -> + Text( + text, + textAlign = textAlign, + modifier = localModifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = 1, + onTextLayout = onTextLayout, + style = style, + ) + } + var offset by remember { mutableStateOf(0) } + val textLayoutInfoState = remember { mutableStateOf(null) } + LaunchedEffect(textLayoutInfoState.value) { + val textLayoutInfo = textLayoutInfoState.value ?: return@LaunchedEffect + if (textLayoutInfo.textWidth <= textLayoutInfo.containerWidth) return@LaunchedEffect + val duration = 7500 * textLayoutInfo.textWidth / textLayoutInfo.containerWidth + val delay = 500L + do { + val animation = TargetBasedAnimation( + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = duration, + delayMillis = 1000, + easing = LinearEasing, + ), + repeatMode = RepeatMode.Restart + ), + typeConverter = Int.VectorConverter, + initialValue = 0, + targetValue = -textLayoutInfo.textWidth + ) + val startTime = withFrameNanos { it } + do { + val playTime = withFrameNanos { it } - startTime + offset = (animation.getValueFromNanos(playTime)) + } while (!animation.isFinishedFromNanos(playTime)) + delay(delay) + } while (true) + } + + SubcomposeLayout( + modifier = modifier.clipToBounds() + ) { constraints -> + val infiniteWidthConstraints = constraints.copy(maxWidth = Int.MAX_VALUE) + var mainText = subcompose(MarqueeLayers.MainText) { + createText(textModifier) + }.first().measure(infiniteWidthConstraints) + + var gradient: Placeable? = null + + var secondPlaceableWithOffset: Pair? = null + if (mainText.width <= constraints.maxWidth) { + offset = 0 + textLayoutInfoState.value = null + } else { + val spacing = constraints.maxWidth * 2 / 3 + textLayoutInfoState.value = TextLayoutInfo( + textWidth = mainText.width + spacing, + containerWidth = constraints.maxWidth + ) + val secondTextOffset = mainText.width + offset + spacing + val secondTextSpace = constraints.maxWidth - secondTextOffset + if (secondTextSpace > 0) { + secondPlaceableWithOffset = subcompose(MarqueeLayers.SecondaryText) { + createText(textModifier) + }.first().measure(infiniteWidthConstraints) to secondTextOffset + } + gradient = subcompose(MarqueeLayers.EdgesGradient) { + Row { + GradientEdge(basicGradientColor, sideGradientColor) + Spacer(Modifier.weight(1f)) + GradientEdge(sideGradientColor, basicGradientColor) + } + }.first().measure(constraints.copy(maxHeight = mainText.height)) + } + + layout( + width = if (mainText.width > constraints.maxWidth) constraints.maxWidth else mainText.width, + height = mainText.height + ) { + mainText.place(offset, 0) + secondPlaceableWithOffset?.let { + it.first.place(it.second, 0) + } + gradient?.place(0, 0) + } + } +} + +@Composable +private fun GradientEdge( + startColor: Color, endColor: Color, +) { + Box( + modifier = Modifier + .width(10.dp) + .fillMaxHeight() + .background( + brush = Brush.horizontalGradient( + listOf(startColor, endColor) + ) + ) + ) +} + + +private enum class MarqueeLayers { MainText, SecondaryText, EdgesGradient } +private data class TextLayoutInfo(val textWidth: Int, val containerWidth: Int) \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/theme/Theme.kt b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/theme/Theme.kt index aebb29cf..592125f3 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/theme/Theme.kt +++ b/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/theme/Theme.kt @@ -1,6 +1,10 @@ package bruhcollective.itaysonlab.jetispot.ui.theme +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper import android.os.Build +import android.view.Window import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* @@ -8,15 +12,28 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp +import androidx.core.view.WindowCompat import com.google.accompanist.systemuicontroller.rememberSystemUiController +private tailrec fun Context.findWindow(): Window? = + when (this) { + is Activity -> window + is ContextWrapper -> baseContext.findWindow() + else -> null + } + @Composable fun ApplicationTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { - val sysUiController = rememberSystemUiController() + val window = LocalView.current.context.findWindow() + val view = LocalView.current + val sysUiController = rememberSystemUiController(window) + + window?.let { WindowCompat.getInsetsController(it, view).isAppearanceLightStatusBars = darkTheme } SideEffect { sysUiController.setSystemBarsColor(color = Color.Transparent, darkIcons = !darkTheme) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 00000000..b9ee96ae --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,144 @@ + + + Inicio + Buscar + Biblioteca + Ajustes + Bienvenido a Jetispot! + Inicia sesión en tu cuenta de Spotify para empezar! + Usuario o correo electrónico + Constraseña + Iniciar sesión + Avisos + Nombre de usuario o contraseña incorrecto + El campo de usuario o contraseña está vacío + Ajustes + Reproductor + Cuenta + Acerca de + Dispositivo + Calidad de audio + Baja + Normal + Alta + Muy alta + Normalización de audio + Deshabilitado + Silenciosa + Normal + Alta + %d segundos + Reproducir canciones sugeridas + Cuando la cola actual termine, se reproducirán canciones en base a tu algoritmo + Cerrar sesión + Sesión iniciada como %s + Ver plan actual + Cuenta premium + Resumen de tu plan + Planes premium + Administrador de planes + Niño + Desconocido + Este plan incluye + Ver otros planes + Versión %s + Ver código fuente + Abrir canal de telegram + Estos ajustes se aplicarán una vez reiniciada la app por completo + La calidad del audio puede variar dependiendo de la canción, el páis/región o algunos testeos a nivel interno de Spotify. Estos ajustes se aplicarán en la próxima canción que escuches. + "Ajusta el nivel de volumen a tu entorno. El ruido puede disminuir la calidad del audio. No afectará cuando esté seleccionado Normal o Silencioso. " + Activar + Cerrar sesión? + Esto eliminará todos los datos de la aplicación de tu dispositivo. Los ajustes no se borrarán. + Confirmar + Cancelar + Historial de escucha + Reproduciendo desde tu bliblioteca + Reproduciendo desde un álbum + Reproduciendo desde un artista + Reproduciendo desde una lista de reproducción + Reproduciendo desde un origen desconocido + Canciones que te gustan + Nuevos episodios + %s canciones + Última vez actualizada el + Filtrar por + Añadido recientemente + Título + Artista + Álbum + Invertir + Listas de reproducción + Álbums + Artistas + Podcasts + Crear un Blend + Generar invitación a grupo + Invita hasta 10 amigos a Blend, una playlist compartida que os recomienda canciones basada en un popurrí de vuestros gustos musicales. + Invitación a Blend + Almacenamiento + Metadatos de las canciones + para acelerar la carga de playlists o cola + Caché temporal + para guardar los datos de las canciones que más escichas + Caché de imágenes + para guardar las carátulas y disminuir el uso de internet + Puedes administrar el caché de la aplicación en esta pantalla. Algunos de los datos son eliminados automaticamente pasado un cierto período de tiempo. + usados + %s en total + Aplicación + Otros + Acciones + Borrar caché de la aplicación + Borrar los metadatos de las canciones + Ha ocurrido un error al cargar esta página :( + Copiar detalles + Recargar + Ocultar contraseña + Mostrar contraseña + Artista principal + Artista secundario + Remixer + Actor + Compositor + Dirigente + Orquesta + Rol desconocido + Inicio + Reciente + Buscar + Biblioteca + Tu biblioteca + Predeterminado del sistema + Para usar Jetispot necesitas una cuenta premium… + Crossfade + Deshabilitado + Miembro del plan + Inglés + Español + Título desconocido + Artista desconocido + Álbum desconocido + Que te gustaría escuchar? + "Canción • " + Lista de reproducción • Personalizada para tí + Lista de reproducción • Hecha por Spotify + "Álbum • " + Artista + No se han encontrado resultados + Cambia tu criterio de búsqueda y vuelve a intentarlo + Lista de reproducción + reproduciendo desde búsqueda + canciones + canciones por + "me gusta • " + Ups, parece que esta canción no tiene letra… + Idioma + Parece que no tienes una cuenta de Spotify Premium. + Configuración de la batería + Ignora la optimización de batería del sistema para esta app para poder mantenerse en segundo plano. + Descartar + Actualización disponible + Actualizar + Escoge un artista + \ 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 f75072ff..27ee9ece 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ Incorrect username or password. To use Jetispot, you must have a Spotify Premium account. Username or password field is empty. - + Jetispot is an unofficial application which is not connected in any ways to Spotify AB.\n\nBy entering your account details and pressing \"Sign in\", you confirm that:\n- you own your account\n- you have an active Spotify Premium subscription\n- you know that Jetispot can\'t download music, save it to offline storage or bypass DRM (and never will be) @@ -29,22 +29,22 @@ Audio quality Low - ~ 24kbit/s + ~ 24kbit/s Normal - ~ 96kbit/s + ~ 96kbit/s High - ~ 160kbit/s + ~ 160kbit/s Very high - ~ 320kbit/s + ~ 320kbit/s Audio normalization Disabled Quiet - -5 dB + -5 dB Normal - 3 dB + 3 dB Loud - 6 dB + 6 dB Crossfade Disabled @@ -136,17 +136,50 @@ Hide password Show password - main artist - featured artist - remixer - actor - composer - conductor - orchestra - unknown role + Main artist + Featured artist + Remixer + Actor + Composer + Conductor + Orchestra + Unknown role Home Recent Browse Library + English + Spanish + System default + Your library + Unknown Title + Unknown Artist + Unknown Album + What would you like to listen? + "Song • " + Playlist • Personalized for you + Playlist • By Spotify + "Playlist" + "Album • " + Artist + Nothing was found + Correct your request and try again + playing from search + songs + songs by + "likes • " + Ups, seems like this song have no lyrics… + Language + Seems like you don\'t have an Spotify Premium account. + Battery configuration + Ignore system\'s battery optimization to be able to keep the app in the background. + Dismiss + Update available + Update + Choose an artist + More options + Go to the artist\'s profile + Add this song to favorites + Share this track \ No newline at end of file diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml new file mode 100644 index 00000000..8c61fdfd --- /dev/null +++ b/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file 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..791000e4 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 46804899..00000000 --- a/build.gradle +++ /dev/null @@ -1,28 +0,0 @@ -buildscript { - ext { - version_code = 13 - version_name = "poc_v13" - - compose_version = "1.3.0-rc01" - compose_m3_version = "1.0.0-rc01" - compose_compiler_version = "1.3.1" - - media2_version = "1.2.1" - accompanist_version = "0.26.5-rc" - room_version = "2.5.0-beta01" - - librespot_commit = "e95c4f0529" - hilt_version = "2.43.2" - } -} - -plugins { - id "com.android.application" version '7.3.1' apply false - id "com.android.library" version '7.3.1' apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false - id "com.google.dagger.hilt.android" version "$hilt_version" apply false -} - -task clean(type: Delete) { - delete rootProject.buildDir -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..e37303e0 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,26 @@ +buildscript { + val version_code by extra(13) + val version_name by extra("poc_v13") + + val compose_version by extra("1.3.0-rc01") + val compose_m3_version by extra("1.0.0-rc01") + val compose_compiler_version by extra("1.3.1") + + val media2_version by extra("1.2.1") + val accompanist_version by extra("0.26.5-rc") + val room_version by extra("2.5.0-beta01") + + val librespot_commit by extra("e95c4f0529") + val hilt_version by extra("2.43.2") +} + +plugins { + id("com.android.application") version "7.3.1" apply false + id("com.android.library") version "7.3.1" apply false + id("org.jetbrains.kotlin.android") version "1.7.10" apply false + id("com.google.dagger.hilt.android") version "2.43.2" apply false +} + +tasks.register("clean", Delete::class) { + delete(rootProject.buildDir) +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle.kts similarity index 75% rename from settings.gradle rename to settings.gradle.kts index 83e105f4..bba2d9f5 100644 --- a/settings.gradle +++ b/settings.gradle.kts @@ -3,7 +3,6 @@ pluginManagement { gradlePluginPortal() google() mavenCentral() - maven { url 'https://jitpack.io' } } } dependencyResolutionManagement { @@ -11,8 +10,8 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven { url 'https://jitpack.io' } + maven ("https://jitpack.io") } } rootProject.name = "Jetispot" -include ':app' +include(":app")