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
+
+
+
+
+## 🔨 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