├── .idea
├── .name
├── .gitignore
├── codeStyles
│ └── codeStyleConfig.xml
├── compiler.xml
├── kotlinc.xml
├── vcs.xml
├── deploymentTargetDropDown.xml
├── migrations.xml
├── gradle.xml
├── misc.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── app
├── .gitignore
└── src
│ └── main
│ ├── proto
│ ├── sp
│ │ ├── context_menu.proto
│ │ ├── DacPlayer.proto
│ │ ├── DacComponent.proto
│ │ ├── home
│ │ │ ├── ExperimentalComponents.proto
│ │ │ ├── CollectionLinksComponents.proto
│ │ │ ├── HomeViewServiceRequest.proto
│ │ │ ├── HeadingComponents.proto
│ │ │ ├── AlbumCardComponents.proto
│ │ │ ├── ArtistCardComponents.proto
│ │ │ ├── TrackCardComponents.proto
│ │ │ ├── EpisodeCardComponents.proto
│ │ │ ├── PlaylistCardComponents.proto
│ │ │ ├── ToolbarComponents2.proto
│ │ │ ├── ShowCardComponents.proto
│ │ │ ├── ToolbarComponents.proto
│ │ │ ├── CoreComponents.proto
│ │ │ ├── PromoComponents.proto
│ │ │ ├── ShortcutsComponents.proto
│ │ │ └── ActionCardsComponents.proto
│ │ ├── AllPlans.proto
│ │ ├── DacComponents.proto
│ │ ├── PremiumPlanRow.proto
│ │ ├── podcastextensions.proto
│ │ ├── collection
│ │ │ ├── extension_descriptor_type.proto
│ │ │ └── collection2v2.proto
│ │ ├── podcast_ratings.proto
│ │ ├── popcount2_external.proto
│ │ └── identity.proto
│ ├── sp_ext
│ │ ├── dac_jetispot.proto
│ │ ├── CollectionUpdate.proto
│ │ ├── contentfeed_client.proto
│ │ ├── searchview.proto
│ │ ├── color_lyrics.proto
│ │ └── contentfeed.proto
│ └── AppConfig.proto
│ ├── res
│ ├── values
│ │ ├── .idea
│ │ │ ├── .gitignore
│ │ │ ├── misc.xml
│ │ │ ├── vcs.xml
│ │ │ └── modules.xml
│ │ ├── themes.xml
│ │ └── plurals.xml
│ ├── raw
│ │ └── keep.xml
│ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ ├── xml
│ │ ├── automotive_app_desc.xml
│ │ └── nsc.xml
│ ├── drawable-xxxhdpi
│ │ ├── round_home_white_24.png
│ │ ├── round_history_white_24.png
│ │ ├── round_assistant_white_24.png
│ │ └── round_library_music_white_24.png
│ ├── values-v29
│ │ └── themes.xml
│ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_sound_wave_outline_28.xml
│ │ └── ic_launcher_foreground.xml
│ └── mipmap-anydpi-v26
│ │ └── ic_launcher.xml
│ └── java
│ └── bruhcollective
│ └── itaysonlab
│ └── jetispot
│ ├── core
│ ├── objs
│ │ ├── gql
│ │ │ ├── GqlWrap.kt
│ │ │ └── ExtractedColors.kt
│ │ ├── misc
│ │ │ └── SpBlendInviteLink.kt
│ │ ├── hub
│ │ │ ├── HubResponse.kt
│ │ │ ├── HubEvent.kt
│ │ │ └── HubItem.kt
│ │ ├── tags
│ │ │ └── ContentFilterResponse.kt
│ │ ├── player
│ │ │ └── PlayFromContextData.kt
│ │ └── external
│ │ │ └── ExternalPersonalizedRecs.kt
│ ├── util
│ │ ├── RfitKtx.kt
│ │ ├── Log.kt
│ │ ├── Revision.kt
│ │ ├── SpUtils.kt
│ │ ├── Device.kt
│ │ └── PlayCommandFactory.kt
│ ├── api
│ │ ├── SpBlendApi.kt
│ │ ├── SpExternalIntegrationApi.kt
│ │ ├── SpColorLyricsApi.kt
│ │ ├── ClientTokenHandler.kt
│ │ ├── SpPartnersApi.kt
│ │ └── SpCollectionApi.kt
│ ├── ext
│ │ └── MetadataExt.kt
│ ├── collection
│ │ ├── db
│ │ │ ├── model2
│ │ │ │ ├── CollectionContentFilter.kt
│ │ │ │ ├── CollectionEntry.kt
│ │ │ │ ├── CollectionArtist.kt
│ │ │ │ ├── CollectionShow.kt
│ │ │ │ ├── CollectionAlbum.kt
│ │ │ │ ├── CollectionEpisode.kt
│ │ │ │ ├── rootlist
│ │ │ │ │ └── CollectionRootlistItem.kt
│ │ │ │ ├── CollectionPinnedItem.kt
│ │ │ │ └── CollectionTrack.kt
│ │ │ ├── model
│ │ │ │ └── LocalCollectionCategory.kt
│ │ │ ├── LocalCollectionRepository.kt
│ │ │ └── LocalCollectionDatabase.kt
│ │ └── paging
│ │ │ └── CollectionAlbumsPagingSource.kt
│ ├── di
│ │ ├── ext
│ │ │ └── DiExt.kt
│ │ ├── ApplicationModule.kt
│ │ └── CollectionModule.kt
│ ├── metadata_db
│ │ └── SpMetadataDb.kt
│ ├── lyrics
│ │ ├── SpLyricsRequester.kt
│ │ └── SpLyricsController.kt
│ ├── SpSessionManager.kt
│ ├── SpAuthManager.kt
│ └── SpConfigurationManager.kt
│ ├── ui
│ ├── shared
│ │ ├── WindowInsets.kt
│ │ ├── dynamic_blocks
│ │ │ ├── DynamicLikeButton.kt
│ │ │ └── DynamicPlayButton.kt
│ │ ├── PagingLoadingPage.kt
│ │ ├── NavController.kt
│ │ └── SharedTexts.kt
│ ├── dac
│ │ ├── components_home
│ │ │ ├── RecentlyPlayedSectionComponentBinder.kt
│ │ │ ├── SectionHeaderComponentBinder.kt
│ │ │ ├── SnappyGridSectionComponentBinder.kt
│ │ │ ├── FilterComponentBinder.kt
│ │ │ ├── ToolbarComponentBinder.kt
│ │ │ └── SmallActionCardBinder.kt
│ │ ├── DacDelegate.kt
│ │ └── components_plans
│ │ │ ├── DisclaimerComponentBinder.kt
│ │ │ └── PlanComponentBinder.kt
│ ├── ext
│ │ ├── ProtoExt.kt
│ │ ├── TopBarExt.kt
│ │ ├── ModifierExt.kt
│ │ ├── ColorExt.kt
│ │ ├── ContextExt.kt
│ │ └── FlowExt.kt
│ ├── hub
│ │ ├── components
│ │ │ ├── ShortcutsContainer.kt
│ │ │ ├── TextRow.kt
│ │ │ ├── HomeSectionHeader.kt
│ │ │ ├── AlbumTrackRow.kt
│ │ │ ├── SectionHeader.kt
│ │ │ ├── ImageRow.kt
│ │ │ ├── FindCard.kt
│ │ │ ├── OutlineButton.kt
│ │ │ ├── LargerRow.kt
│ │ │ ├── ArtistPinnedItem.kt
│ │ │ ├── ShortcutsCard.kt
│ │ │ ├── HomeSectionLargeHeader.kt
│ │ │ ├── SingleFocusCard.kt
│ │ │ ├── Carousel.kt
│ │ │ ├── MediumCard.kt
│ │ │ ├── ArtistTrackRow.kt
│ │ │ ├── ArtistHeader.kt
│ │ │ ├── GridMediumCard.kt
│ │ │ ├── PodcastTopicsStrip.kt
│ │ │ ├── PlaylistTrackRow.kt
│ │ │ └── ShowHeader.kt
│ │ ├── HubScreenDelegate.kt
│ │ ├── HubEventHandler.kt
│ │ ├── virt
│ │ │ ├── CollectionEntityView.kt
│ │ │ └── ShowEntityView.kt
│ │ └── HubBinder.kt
│ ├── monet
│ │ ├── google
│ │ │ ├── utils
│ │ │ │ └── StringUtils.java
│ │ │ └── palettes
│ │ │ │ └── TonalPalette.java
│ │ └── ColorToScheme.kt
│ ├── blocks
│ │ └── TwoColumnAndImageBlock.kt
│ ├── theme
│ │ └── Theme.kt
│ ├── navigation
│ │ └── NavigationController.kt
│ └── screens
│ │ ├── hub
│ │ ├── BrowseScreen.kt
│ │ ├── BrowseRadioScreen.kt
│ │ ├── PodcastShowScreen.kt
│ │ ├── AbsHubViewModel.kt
│ │ └── PlaylistScreen.kt
│ │ ├── search
│ │ └── SearchViewModel.kt
│ │ ├── blend
│ │ └── BlendCreateInvitationScreen.kt
│ │ ├── Screen.kt
│ │ ├── nowplaying
│ │ └── NowPlayingScreen.kt
│ │ ├── auth
│ │ └── AuthScreenViewModel.kt
│ │ ├── history
│ │ └── ListeningHistoryScreen.kt
│ │ └── dynamic
│ │ └── DynamicSpIdScreen.kt
│ ├── ManageStorageActivity.kt
│ ├── playback
│ ├── helpers
│ │ ├── MediaItemWrapper.kt
│ │ └── MediaItemBuilder.kt
│ ├── service
│ │ ├── refl
│ │ │ └── SpReflect.kt
│ │ └── AudioFocusManager.kt
│ └── sp
│ │ └── LowToHighQualityPicker.kt
│ └── SpApp.kt
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
├── gradle.properties
└── README.md
/.idea/.name:
--------------------------------------------------------------------------------
1 | Jetispot
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/app/src/main/proto/sp/context_menu.proto:
--------------------------------------------------------------------------------
1 | // com.spotify.home.dac.contextMenu.v1.proto
--------------------------------------------------------------------------------
/app/src/main/res/values/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/app/src/main/res/raw/keep.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTaysonLab/jetispot/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTaysonLab/jetispot/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTaysonLab/jetispot/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTaysonLab/jetispot/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTaysonLab/jetispot/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTaysonLab/jetispot/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/xml/automotive_app_desc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/round_home_white_24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTaysonLab/jetispot/HEAD/app/src/main/res/drawable-xxxhdpi/round_home_white_24.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/round_history_white_24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTaysonLab/jetispot/HEAD/app/src/main/res/drawable-xxxhdpi/round_history_white_24.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/round_assistant_white_24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTaysonLab/jetispot/HEAD/app/src/main/res/drawable-xxxhdpi/round_assistant_white_24.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/round_library_music_white_24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTaysonLab/jetispot/HEAD/app/src/main/res/drawable-xxxhdpi/round_library_music_white_24.png
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/gql/GqlWrap.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.objs.gql
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | class GqlWrap (
7 | val data: T
8 | )
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/WindowInsets.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.shared
2 |
3 | import androidx.compose.foundation.layout.WindowInsets
4 | import androidx.compose.ui.unit.dp
5 |
6 | val EmptyWindowInsets = WindowInsets(top = 0.dp)
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/misc/SpBlendInviteLink.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.objs.misc
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | class SpBlendInviteLink(
7 | val invite: String
8 | )
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Sep 26 19:18:25 CEST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/RfitKtx.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.util
2 |
3 | import retrofit2.Retrofit
4 | import retrofit2.create
5 |
6 | inline fun Retrofit.create(baseUrl: String) = newBuilder().baseUrl(baseUrl).build().create()
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values/plurals.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - %d second
5 | - %d seconds
6 | - %d seconds
7 |
8 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v29/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_home/RecentlyPlayedSectionComponentBinder.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.dac.components_home
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | @Composable
6 | fun RecentlyPlayedSectionComponentBinder() {
7 | // TODO: lazy load recents
8 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/DacPlayer.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/any.proto";
4 |
5 | package com.spotify.dac.player.v1.proto;
6 |
7 | option java_multiple_files = true;
8 | option java_package = "com.spotify.dac.player.v1.proto";
9 |
10 | message PlayCommand {
11 | bytes context = 1;
12 | bytes options = 2;
13 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/DacComponent.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/any.proto";
4 |
5 | package dac.api.components;
6 |
7 | option java_multiple_files = true;
8 | option java_package = "com.spotify.dac.api.components";
9 |
10 | message VerticalListComponent {
11 | repeated google.protobuf.Any components = 2;
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/api/SpBlendApi.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.api
2 |
3 | import bruhcollective.itaysonlab.jetispot.core.objs.misc.SpBlendInviteLink
4 | import retrofit2.http.POST
5 |
6 | interface SpBlendApi {
7 | @POST("/blend-invitation/v1/generate")
8 | suspend fun generateBlend(): SpBlendInviteLink
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/ext/MetadataExt.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.ext
2 |
3 | import bruhcollective.itaysonlab.jetispot.core.util.SpUtils
4 | import com.spotify.metadata.Metadata
5 |
6 | val Metadata.Track.imageUrl: String? get() = SpUtils.getImageUrl(this.album.coverGroup.imageList.first { it.size == Metadata.Image.Size.DEFAULT }.fileId)
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/db/model2/CollectionContentFilter.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.db.model2
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "lcFilters")
7 | data class CollectionContentFilter(
8 | @PrimaryKey val name: String,
9 | val query: String
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/hub/HubResponse.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.objs.hub
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | class HubResponse (
7 | val title: String? = null,
8 | val header: HubItem? = null,
9 | val body: List,
10 | val id: String? = null, // album-entity-view
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/ext/ProtoExt.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.ext
2 |
3 | import com.google.protobuf.Any
4 | import com.google.protobuf.Message
5 |
6 | @Suppress("UNCHECKED_CAST")
7 | fun Any.dynamicUnpack(): Message = unpack(Class.forName(typeUrl.split("/")[1].let {
8 | if (!it.startsWith("com.spotify")) "com.spotify.${it}" else it
9 | }) as Class)
--------------------------------------------------------------------------------
/app/src/main/proto/sp_ext/dac_jetispot.proto:
--------------------------------------------------------------------------------
1 | // Сustom DAC blocks for Jetispot usage
2 | syntax = "proto3";
3 |
4 | option java_package = "bruhcollective.itaysonlab.jetispot.proto";
5 | option java_multiple_files = true;
6 |
7 | message ErrorComponent {
8 | enum ErrorType {
9 | UNSUPPORTED = 0;
10 | GENERIC_EXCEPTION = 1;
11 | }
12 |
13 | ErrorType type = 1;
14 | string message = 2;
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/db/model2/CollectionEntry.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.db.model2
2 |
3 | interface CollectionEntry {
4 | fun ceId(): String
5 | fun ceUri(): String
6 | fun ceTimestamp(): Long
7 | fun ceModifyPredef(type: PredefCeType, dyn: String) {}
8 | }
9 |
10 | enum class PredefCeType {
11 | COLLECTION, EPISODES
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/di/ext/DiExt.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.di.ext
2 |
3 | import okhttp3.OkHttpClient
4 | import okhttp3.Request
5 |
6 | fun OkHttpClient.Builder.interceptRequest(scope: Request.Builder.(Request) -> Unit) = addInterceptor { chain ->
7 | val request = chain.request()
8 | chain.proceed(request.newBuilder().apply { scope(this, request) }.build())
9 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/ExperimentalComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.spotify.home.dac.component.experimental.v1.proto;
4 |
5 | option java_multiple_files = true;
6 | option java_package = "com.spotify.home.dac.component.experimental.v1.proto";
7 |
8 | message FilterComponent {
9 | repeated Facet facets = 1;
10 | }
11 |
12 | message Facet {
13 | string title = 1;
14 | string value = 2;
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/db/model/LocalCollectionCategory.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.db.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "lcTypes")
7 | data class LocalCollectionCategory(
8 | @PrimaryKey val type: String,
9 | val syncToken: String // or revision in case of rootList
10 | )
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/CollectionLinksComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.spotify.home.dac.component.v1.proto;
4 |
5 | option java_multiple_files = true;
6 | option java_package = "com.spotify.home.dac.component.v1.proto";
7 |
8 | message LikedSongsCardMediumComponent {}
9 | message LikedSongsCardSmallComponent {}
10 |
11 | message YourEpisodesCardMediumComponent {}
12 | message YourEpisodesCardSmallComponent {}
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/HomeViewServiceRequest.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/any.proto";
4 |
5 | package com.spotify.home.dac.viewservice.v1.proto;
6 |
7 | option java_multiple_files = true;
8 | option java_package = "com.spotify.home.dac.viewservice.v1.proto";
9 |
10 | message HomeViewServiceRequest {
11 | string facet = 1;
12 | string client_timezone = 2;
13 | map feature_flags = 3;
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/tags/ContentFilterResponse.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.objs.tags
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | class ContentFilterResponse(
7 | val contentFilters: List
8 | )
9 |
10 | @JsonClass(generateAdapter = true)
11 | class ContentFilter(
12 | val title: String,
13 | val query: String
14 | )
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/proto/sp/AllPlans.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.spotify.allplans.v1;
4 |
5 | option java_package = "com.spotify.allplans.v1";
6 | option java_multiple_files = true;
7 |
8 | message PlanComponent {
9 | string planName = 1;
10 | string planColor = 2;
11 | string availableAccounts = 3;
12 | string planPrice = 4;
13 | string uri = 5;
14 | }
15 |
16 | message DisclaimerComponent {
17 | string text = 1;
18 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/nsc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | spotify.com
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShortcutsContainer.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
5 | import bruhcollective.itaysonlab.jetispot.ui.hub.HubBinder
6 |
7 | @Composable
8 | fun ShortcutsContainer (
9 | children: List
10 | ) {
11 | children.forEach { item ->
12 | HubBinder(item)
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/DacDelegate.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.dac
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.compose.runtime.staticCompositionLocalOf
5 | import com.spotify.dac.player.v1.proto.PlayCommand
6 |
7 | @Stable
8 | interface DacDelegate {
9 | fun dispatchPlay(command: PlayCommand)
10 | }
11 |
12 | val LocalDacDelegate = staticCompositionLocalOf { error("DacDelegate should be initialized") }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | maven { url 'https://jitpack.io' }
7 | }
8 | }
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | maven { url 'https://jitpack.io' }
15 | }
16 | }
17 | rootProject.name = "Jetispot"
18 | include ':app'
19 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/ext/TopBarExt.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.ext
2 |
3 | import androidx.compose.material3.ExperimentalMaterial3Api
4 | import androidx.compose.material3.TopAppBarDefaults
5 | import androidx.compose.material3.TopAppBarScrollBehavior
6 | import androidx.compose.runtime.Composable
7 |
8 | @OptIn(ExperimentalMaterial3Api::class)
9 | @Composable
10 | fun rememberEUCScrollBehavior(): TopAppBarScrollBehavior {
11 | return TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/db/model2/CollectionArtist.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.db.model2
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "lcArtists")
7 | data class CollectionArtist(
8 | @PrimaryKey val id: String,
9 | val uri: String,
10 | val name: String,
11 | val picture: String,
12 | val addedAt: Int
13 | ): CollectionEntry {
14 | override fun ceId() = id
15 | override fun ceUri() = uri
16 | override fun ceTimestamp() = addedAt.toLong()
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/db/model2/CollectionShow.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.db.model2
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "lcShows")
7 | data class CollectionShow(
8 | @PrimaryKey val uri: String,
9 | val name: String,
10 | val publisher: String,
11 | val picture: String,
12 | val addedAt: Int
13 | ): CollectionEntry {
14 | override fun ceId() = uri
15 | override fun ceUri() = uri
16 | override fun ceTimestamp() = addedAt.toLong()
17 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/HeadingComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/any.proto";
4 |
5 | package com.spotify.home.dac.component.heading.v1.proto;
6 |
7 | option java_multiple_files = true;
8 | option java_package = "com.spotify.home.dac.component.heading.v1.proto";
9 |
10 | message RecsplanationHeadingSingleTextComponent {
11 | HighlightedText highlighted_text = 1;
12 | string navigate_uri = 2;
13 | string image_uri = 3;
14 | }
15 |
16 | message HighlightedText {
17 | string text = 1;
18 | int32 start_inclusive = 2;
19 | int32 end_exclusive = 3;
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/Log.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.util
2 |
3 | import bruhcollective.itaysonlab.jetispot.BuildConfig
4 |
5 | object Log {
6 | fun d(tag: String, message: String) = dbg { android.util.Log.d(tag(tag), message) }
7 | fun w(tag: String, message: String) = dbg { android.util.Log.w(tag(tag), message) }
8 | fun e(tag: String, message: String) = dbg { android.util.Log.e(tag(tag), message) }
9 |
10 | private fun tag (tag: String) = "Sp:$tag"
11 | private fun dbg (ifdbg: () -> Unit) = if (BuildConfig.DEBUG) ifdbg() else Unit
12 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/DacComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/any.proto";
4 |
5 | package dac.api.v1.proto;
6 |
7 | option java_multiple_files = true;
8 | option java_package = "com.spotify.dac.api.v1.proto";
9 |
10 | message DacResponse {
11 | google.protobuf.Any component = 2;
12 | }
13 |
14 | message DacRequest {
15 | string uri = 2;
16 | google.protobuf.Any featureRequest = 3;
17 | ClientInfo clientInfo = 4;
18 |
19 | message ClientInfo {
20 | string appName = 1; // ANDROID_MUSIC_APP
21 | string version = 2;
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/db/model2/CollectionAlbum.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.db.model2
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "lcAlbums")
7 | data class CollectionAlbum(
8 | @PrimaryKey val id: String,
9 | val uri: String,
10 | val name: String,
11 | val rawArtistsData: String,
12 | val picture: String,
13 | val addedAt: Int
14 | ): CollectionEntry {
15 | override fun ceId() = id
16 | override fun ceUri() = uri
17 | override fun ceTimestamp() = addedAt.toLong()
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/gql/ExtractedColors.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.objs.gql
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | class ExtractedColors(
7 | val extractedColors: List
8 | )
9 |
10 | @JsonClass(generateAdapter = true)
11 | class ExtractedColorsResponse(
12 | val colorRaw: ExtractedColor,
13 | val colorLight: ExtractedColor,
14 | val colorDark: ExtractedColor,
15 | )
16 |
17 | @JsonClass(generateAdapter = true)
18 | class ExtractedColor(
19 | val hex: String,
20 | val isFallback: Boolean
21 | )
--------------------------------------------------------------------------------
/app/src/main/proto/sp/PremiumPlanRow.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package spotify.pamviewservice.v1.proto;
4 |
5 | option java_package = "com.spotify.pamviewservice.v1.proto";
6 |
7 | enum SubscriptionType {
8 | UNKNOWN = 0;
9 | RECURRING_MONTHLY = 1;
10 | TRIAL = 2;
11 | PREPAID = 3;
12 | }
13 |
14 | message AllPremiumPlansRow {
15 | bool is_trial = 1;
16 | bool is_prepaid = 2;
17 | string premium_plan = 3;
18 | string premium_plan_color = 4;
19 | uint32 prepaid_days_remaining = 5;
20 | SubscriptionType subscription_type = 6;
21 | repeated string available_plan_names = 7;
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/api/SpExternalIntegrationApi.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.api
2 |
3 | import bruhcollective.itaysonlab.jetispot.core.objs.external.PersonalizedRecommendationsRequest
4 | import bruhcollective.itaysonlab.jetispot.core.objs.external.PersonalizedRecommendationsResponse
5 | import retrofit2.http.Body
6 | import retrofit2.http.POST
7 |
8 | interface SpExternalIntegrationApi {
9 | @POST("v2/personalized-recommendations")
10 | suspend fun personalizedRecommendations(
11 | @Body body: PersonalizedRecommendationsRequest
12 | ): PersonalizedRecommendationsResponse
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/db/model2/CollectionEpisode.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.db.model2
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "lcEpisodes")
7 | data class CollectionEpisode(
8 | @PrimaryKey val uri: String,
9 | val name: String,
10 | val description: String,
11 | val showName: String,
12 | val showUri: String,
13 | val picture: String,
14 | val addedAt: Int
15 | ): CollectionEntry {
16 | override fun ceId() = uri
17 | override fun ceUri() = uri
18 | override fun ceTimestamp() = addedAt.toLong()
19 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp_ext/CollectionUpdate.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/any.proto";
4 |
5 | package bruhcollective.itaysonlab.swedentricks.protos;
6 |
7 | option java_multiple_files = true;
8 | option java_package = "bruhcollective.itaysonlab.swedentricks.protos";
9 |
10 | message CollectionUpdate {
11 | repeated CollectionUpdateEntry items = 1;
12 | }
13 |
14 | message CollectionUpdateEntry {
15 | enum Type {
16 | TRACK = 0;
17 | ALBUM = 1;
18 | ARTIST = 4;
19 | SHOW = 5;
20 | EPISODE = 6;
21 | }
22 |
23 | Type type = 1;
24 | bytes identifier = 2;
25 | int32 addedAt = 5;
26 | bool removed = 6;
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/api/SpColorLyricsApi.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.api
2 |
3 | import com.spotify.lyrics.v2.lyrics.proto.ColorLyricsResponse
4 | import retrofit2.http.GET
5 | import retrofit2.http.Path
6 | import retrofit2.http.Query
7 |
8 | interface SpColorLyricsApi {
9 | @GET("/color-lyrics/v2/track/{spotifyId}/")
10 | suspend fun getLyrics(
11 | @Path("spotifyId") spotifyId: String,
12 | @Query("vocalRemoval") vocalRemoval: Boolean = false,
13 | @Query("syllableSync") syllableSync: Boolean = false,
14 | @Query("clientLanguage") clientLanguage: String = "en_US",
15 | ): ColorLyricsResponse
16 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/podcastextensions.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package spotify.podcast.extensions;
4 |
5 | option objc_class_prefix = "SPT";
6 | option java_multiple_files = true;
7 | option optimize_for = CODE_SIZE;
8 | option java_outer_classname = "PodcastExtensionsProto";
9 | option java_package = "com.spotify.podcastextensions.proto";
10 |
11 | message PodcastTopics {
12 | repeated PodcastTopic topics = 1;
13 | }
14 |
15 | message PodcastTopic {
16 | string uri = 1;
17 | string title = 2;
18 | }
19 |
20 | message PodcastHtmlDescription {
21 | Header header = 1;
22 | message Header {
23 |
24 | }
25 |
26 | string html_description = 2;
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/AlbumCardComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.spotify.home.dac.component.v1.proto;
4 |
5 | option java_multiple_files = true;
6 | option java_package = "com.spotify.home.dac.component.v1.proto";
7 |
8 | message AlbumCardSmallComponent {
9 | string title = 1;
10 | string navigate_uri = 2;
11 | string image_uri = 3;
12 | }
13 |
14 | message AlbumCardMediumComponent {
15 | string title = 1;
16 | string subtitle = 2;
17 | string navigate_uri = 3;
18 | string image_uri = 4;
19 | }
20 |
21 | message AlbumCardLargeComponent {
22 | string title = 1;
23 | string subtitle = 2;
24 | string navigate_uri = 3;
25 | string image_uri = 4;
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/ArtistCardComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.spotify.home.dac.component.v1.proto;
4 |
5 | option java_multiple_files = true;
6 | option java_package = "com.spotify.home.dac.component.v1.proto";
7 |
8 | message ArtistCardSmallComponent {
9 | string title = 1;
10 | string navigate_uri = 2;
11 | string image_uri = 3;
12 | }
13 |
14 | message ArtistCardMediumComponent {
15 | string title = 1;
16 | string subtitle = 2;
17 | string navigate_uri = 3;
18 | string image_uri = 4;
19 | }
20 |
21 | message ArtistCardLargeComponent {
22 | string title = 1;
23 | string subtitle = 2;
24 | string navigate_uri = 3;
25 | string image_uri = 4;
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/TrackCardComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.spotify.home.dac.component.v1.proto;
4 |
5 | option java_multiple_files = true;
6 | option java_package = "com.spotify.home.dac.component.v1.proto";
7 |
8 | message TrackCardSmallComponent {
9 | string title = 1;
10 | string navigate_uri = 2;
11 | string image_uri = 3;
12 | }
13 |
14 | message TrackCardMediumComponent {
15 | string title = 1;
16 | string subtitle = 2;
17 | string navigate_uri = 3;
18 | string image_uri = 4;
19 | }
20 |
21 | message TrackCardLargeComponent {
22 | string title = 1;
23 | string subtitle = 2;
24 | string navigate_uri = 3;
25 | string image_uri = 4;
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/EpisodeCardComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.spotify.home.dac.component.v1.proto;
4 |
5 | option java_multiple_files = true;
6 | option java_package = "com.spotify.home.dac.component.v1.proto";
7 |
8 | message EpisodeCardSmallComponent {
9 | string title = 1;
10 | string navigate_uri = 2;
11 | string image_uri = 3;
12 | }
13 |
14 | message EpisodeCardMediumComponent {
15 | string title = 1;
16 | string subtitle = 2;
17 | string navigate_uri = 3;
18 | string image_uri = 4;
19 | }
20 |
21 | message EpisodeCardLargeComponent {
22 | string title = 1;
23 | string subtitle = 2;
24 | string navigate_uri = 3;
25 | string image_uri = 4;
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/PlaylistCardComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.spotify.home.dac.component.v1.proto;
4 |
5 | option java_multiple_files = true;
6 | option java_package = "com.spotify.home.dac.component.v1.proto";
7 |
8 | message PlaylistCardSmallComponent {
9 | string title = 1;
10 | string navigate_uri = 2;
11 | string image_uri = 3;
12 | }
13 |
14 | message PlaylistCardMediumComponent {
15 | string title = 1;
16 | string subtitle = 2;
17 | string navigate_uri = 3;
18 | string image_uri = 4;
19 | }
20 |
21 | message PlaylistCardLargeComponent {
22 | string title = 1;
23 | string subtitle = 2;
24 | string navigate_uri = 3;
25 | string image_uri = 4;
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/db/model2/rootlist/CollectionRootlistItem.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.db.model2.rootlist
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import bruhcollective.itaysonlab.jetispot.core.collection.db.model2.CollectionEntry
6 |
7 | @Entity(tableName = "rootlist")
8 | data class CollectionRootlistItem(
9 | @PrimaryKey val uri: String,
10 | val timestamp: Long,
11 | val name: String,
12 | val ownerUsername: String,
13 | val picture: String
14 | ): CollectionEntry {
15 | override fun ceId() = uri
16 | override fun ceUri() = uri
17 | override fun ceTimestamp() = timestamp / 1000L
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_plans/DisclaimerComponentBinder.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.dac.components_plans
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.unit.dp
8 | import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext
9 | import com.spotify.allplans.v1.DisclaimerComponent
10 |
11 | @Composable
12 | fun DisclaimerComponentBinder(
13 | item: DisclaimerComponent
14 | ) {
15 | Column {
16 | Subtext(text = item.text, modifier = Modifier.padding(start = 16.dp))
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/dynamic_blocks/DynamicLikeButton.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.shared.dynamic_blocks
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.rounded.Favorite
5 | import androidx.compose.material3.Icon
6 | import androidx.compose.material3.IconButton
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 |
10 | @Composable
11 | fun DynamicLikeButton(
12 | objectUrl: String,
13 | modifier: Modifier = Modifier
14 | ) {
15 | IconButton(onClick = {
16 | // todo
17 | }, modifier = modifier) {
18 | Icon(Icons.Rounded.Favorite, contentDescription = null)
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/collection/extension_descriptor_type.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package spotify.descriptorextension;
4 |
5 | option java_multiple_files = true;
6 | option optimize_for = CODE_SIZE;
7 | option java_package = "com.spotify.descriptorextension.proto";
8 |
9 | message ExtensionDescriptor {
10 | string text = 1;
11 | float weight = 2;
12 | repeated ExtensionDescriptorType types = 3;
13 | }
14 |
15 | message ExtensionDescriptorData {
16 | repeated ExtensionDescriptor descriptors = 1;
17 | }
18 |
19 | enum ExtensionDescriptorType {
20 | UNKNOWN = 0;
21 | GENRE = 1;
22 | MOOD = 2;
23 | ACTIVITY = 3;
24 | INSTRUMENT = 4;
25 | TIME = 5;
26 | ERA = 6;
27 | AESTHETIC = 7;
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/ext/ModifierExt.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.ext
2 |
3 | import androidx.compose.ui.Modifier
4 | import androidx.compose.ui.input.pointer.PointerEventPass
5 | import androidx.compose.ui.input.pointer.PointerInputChange
6 | import androidx.compose.ui.input.pointer.pointerInput
7 |
8 | fun Modifier.disableTouch(disabled: Boolean = true) =
9 | if (disabled) {
10 | pointerInput(Unit) {
11 | awaitPointerEventScope {
12 | while (true) {
13 | awaitPointerEvent(pass = PointerEventPass.Initial).changes.forEach(PointerInputChange::consume)
14 | }
15 | }
16 | }
17 | } else {
18 | Modifier
19 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/ToolbarComponents2.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/any.proto";
4 | import "sp/DacPlayer.proto";
5 |
6 | package com.spotify.home.dac.component.v2.proto;
7 |
8 | option java_multiple_files = true;
9 | option java_package = "com.spotify.home.dac.component.v2.proto";
10 |
11 | message ToolbarComponentV2 {
12 | string day_part_message = 1;
13 | string subtitle = 2;
14 | repeated google.protobuf.Any items = 3;
15 | ToolbarItemProfileComponent profile_button = 4;
16 | }
17 |
18 | message ToolbarItemProfileComponent {
19 | string accessibility_title = 1;
20 | string username = 2;
21 | string user_first_initial = 3;
22 | string navigate_uri = 4;
23 | string image_uri = 5;
24 | string user_display_name = 6;
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/paging/CollectionAlbumsPagingSource.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.paging
2 |
3 | import androidx.paging.PagingSource
4 | import androidx.paging.PagingState
5 | import bruhcollective.itaysonlab.jetispot.core.collection.SpCollectionManager
6 | import bruhcollective.itaysonlab.jetispot.core.collection.db.model2.CollectionAlbum
7 |
8 | class CollectionAlbumsPagingSource(private val collection: SpCollectionManager): PagingSource() {
9 | override suspend fun load(params: LoadParams): LoadResult {
10 | TODO()
11 | }
12 |
13 | override fun getRefreshKey(state: PagingState): Int? {
14 | TODO()
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/PagingLoadingPage.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.shared
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.size
6 | import androidx.compose.material3.CircularProgressIndicator
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.unit.dp
11 |
12 | @Composable
13 | fun PagingLoadingPage (
14 | modifier: Modifier
15 | ) {
16 | Box(modifier) {
17 | CircularProgressIndicator(
18 | modifier = Modifier
19 | .align(Alignment.Center)
20 | .size(56.dp)
21 | )
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/proto/AppConfig.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_package = "bruhcollective.itaysonlab.jetispot.proto";
4 | option java_multiple_files = true;
5 |
6 | message AppConfig {
7 | PlayerConfig playerConfig = 1;
8 | }
9 |
10 | message PlayerConfig {
11 | bool autoplay = 1;
12 | bool normalization = 2;
13 | AudioQuality preferredQuality = 3;
14 | AudioNormalization normalizationLevel = 4;
15 | int32 crossfade = 5; // 12s - max
16 | bool preload = 6;
17 | bool useTremolo = 7;
18 | }
19 |
20 | enum AudioQuality {
21 | NORMAL = 0;
22 | HIGH = 1;
23 | VERY_HIGH = 2;
24 | LOW = 3;
25 | FLAC = 4;
26 | }
27 |
28 | // (loud at +6, normal at +3, quiet at -5)
29 | enum AudioNormalization {
30 | QUIET = 0;
31 | BALANCED = 1;
32 | LOUD = 2;
33 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/ShowCardComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.spotify.home.dac.component.v1.proto;
4 |
5 | option java_multiple_files = true;
6 | option java_package = "com.spotify.home.dac.component.v1.proto";
7 |
8 | message ShowCardSmallComponent {
9 | string title = 1;
10 | string show_categories = 2;
11 | string navigate_uri = 3;
12 | string image_uri = 4;
13 | }
14 |
15 | message ShowCardMediumComponent {
16 | string title = 1;
17 | string subtitle = 2;
18 | string show_categories = 3;
19 | string navigate_uri = 4;
20 | string image_uri = 5;
21 | }
22 |
23 | message ShowCardLargeComponent {
24 | string title = 1;
25 | string subtitle = 2;
26 | string show_categories = 3;
27 | string navigate_uri = 4;
28 | string image_uri = 5;
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/ToolbarComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/any.proto";
4 | import "sp/DacPlayer.proto";
5 |
6 | package com.spotify.home.dac.component.v1.proto;
7 |
8 | option java_multiple_files = true;
9 | option java_package = "com.spotify.home.dac.component.v1.proto";
10 |
11 | message ToolbarComponent {
12 | string day_part_message = 1;
13 | string subtitle = 2;
14 | repeated google.protobuf.Any items = 4;
15 | }
16 |
17 | message ToolbarItemFeedComponent {
18 | string title = 1;
19 | string navigate_uri = 2;
20 | }
21 |
22 | message ToolbarItemListeningHistoryComponent {
23 | string title = 1;
24 | string navigate_uri = 2;
25 | }
26 |
27 | message ToolbarItemSettingsComponent {
28 | string title = 1;
29 | string navigate_uri = 2;
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/TextRow.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.unit.dp
9 | import androidx.compose.ui.unit.sp
10 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubText
11 |
12 | @Composable
13 | fun TextRow(
14 | text: HubText
15 | ) {
16 | Text(text.title ?: text.description ?: "", color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (text.description == null) 1f else 0.7f), fontSize = 16.sp, modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp))
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/metadata_db/SpMetadataDb.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.metadata_db
2 |
3 | import android.content.Context
4 | import com.google.protobuf.ByteString
5 | import com.tencent.mmkv.MMKV
6 | import dagger.hilt.android.qualifiers.ApplicationContext
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | @Singleton
11 | class SpMetadataDb @Inject constructor(
12 | @ApplicationContext private val context: Context
13 | ) {
14 | private val instance = MMKV.mmkvWithID("metadata")
15 |
16 | fun contains(uri: String) = instance.containsKey(uri)
17 |
18 | fun get(uri: String): ByteArray = instance.getBytes(uri, null)!!
19 | fun put(uri: String, msg: ByteArray) = instance.encode(uri, msg)
20 |
21 | fun clear() = instance.clearAll()
22 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/podcast_ratings.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package spotify.ratings;
4 |
5 | import "google/protobuf/timestamp.proto";
6 |
7 | option objc_class_prefix = "SPT";
8 | option java_multiple_files = true;
9 | option optimize_for = CODE_SIZE;
10 | option java_outer_classname = "RatingsMetadataProto";
11 | option java_package = "com.spotify.podcastcreatorinteractivity.v1";
12 |
13 | message Rating {
14 | string user_id = 1;
15 | string show_uri = 2;
16 | int32 rating = 3;
17 | google.protobuf.Timestamp rated_at = 4;
18 | }
19 |
20 | message AverageRating {
21 | double average = 1;
22 | int64 total_ratings = 2;
23 | bool show_average = 3;
24 | }
25 |
26 | message PodcastRating {
27 | AverageRating average_rating = 1;
28 | Rating rating = 2;
29 | bool can_rate = 3;
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/proto/sp_ext/contentfeed_client.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "sp_ext/contentfeed.proto";
4 |
5 | package com.spotify.contentfeed.proto.v1.client;
6 |
7 | option java_multiple_files = true;
8 | option java_package = "com.spotify.contentfeed.proto.v1.client";
9 |
10 | message FeedItemsResponse {
11 | repeated com.spotify.contentfeed.proto.v1.common.FeedItem items = 1;
12 | string requestId = 2;
13 | }
14 |
15 | message FeedItemsStateRequest {
16 | repeated com.spotify.contentfeed.proto.v1.common.FeedItemState items = 1;
17 | }
18 |
19 | message FeedItemsRequest {
20 | enum Filter {
21 | FILTER_INVALID = 0;
22 | FILTER_PODCAST_EPISODE_RELEASE_UNPLAYED = 1;
23 | }
24 |
25 | repeated com.spotify.contentfeed.proto.v1.common.ContentType contentTypes = 1;
26 | repeated Filter filters = 2;
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/db/model2/CollectionPinnedItem.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.db.model2
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "lcPins")
7 | data class CollectionPinnedItem(
8 | @PrimaryKey val uri: String,
9 | val name: String,
10 | val subtitle: String,
11 | val picture: String,
12 | val addedAt: Int
13 | ): CollectionEntry {
14 | @Transient var predefType: PredefCeType? = null
15 | @Transient var predefDyn: String = ""
16 |
17 | override fun ceId() = uri
18 | override fun ceUri() = uri
19 | override fun ceTimestamp() = addedAt.toLong()
20 |
21 | override fun ceModifyPredef(type: PredefCeType, dyn: String) {
22 | predefType = type
23 | predefDyn = dyn
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ManageStorageActivity.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.core.view.WindowCompat
7 | import bruhcollective.itaysonlab.jetispot.ui.screens.config.StorageScreen
8 | import bruhcollective.itaysonlab.jetispot.ui.theme.ApplicationTheme
9 | import dagger.hilt.android.AndroidEntryPoint
10 |
11 | @AndroidEntryPoint
12 | class ManageStorageActivity: ComponentActivity() {
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 |
16 | WindowCompat.setDecorFitsSystemWindows(window, false)
17 |
18 | setContent {
19 | ApplicationTheme {
20 | StorageScreen()
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/di/ApplicationModule.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.di
2 |
3 | import android.content.Context
4 | import android.content.pm.PackageManager
5 | import android.content.res.Resources
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.android.qualifiers.ApplicationContext
10 | import dagger.hilt.components.SingletonComponent
11 |
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 | object ApplicationModule {
15 | @Provides
16 | fun provideResources(@ApplicationContext context: Context): Resources {
17 | return context.resources
18 | }
19 |
20 | @Provides
21 | fun providePackageManager(@ApplicationContext context: Context): PackageManager {
22 | return context.packageManager
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/hub/HubEvent.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.objs.hub
2 |
3 | import bruhcollective.itaysonlab.jetispot.core.objs.player.PlayFromContextData
4 | import com.squareup.moshi.JsonClass
5 | import dev.zacsweers.moshix.sealed.annotations.DefaultObject
6 | import dev.zacsweers.moshix.sealed.annotations.TypeLabel
7 |
8 | @JsonClass(generateAdapter = true, generator = "sealed:name")
9 | sealed class HubEvent {
10 | @JsonClass(generateAdapter = true)
11 | @TypeLabel("navigate")
12 | class NavigateToUri (
13 | val data: NavigateUri
14 | ): HubEvent()
15 |
16 | @JsonClass(generateAdapter = true)
17 | @TypeLabel("playFromContext")
18 | class PlayFromContext (
19 | val data: PlayFromContextData
20 | ): HubEvent()
21 |
22 | @DefaultObject
23 | object Unknown: HubEvent()
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_home/SectionHeaderComponentBinder.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.dac.components_home
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.Text
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.text.font.FontWeight
11 | import androidx.compose.ui.unit.dp
12 | import androidx.compose.ui.unit.sp
13 |
14 | @Composable
15 | fun SectionHeaderComponentBinder (
16 | text: String
17 | ) {
18 | Text(text = text, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 21.sp, modifier = Modifier.padding(16.dp))
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/api/ClientTokenHandler.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.api
2 |
3 | import bruhcollective.itaysonlab.jetispot.core.SpSessionManager
4 | import com.spotify.clienttoken.http.v0.ClientToken
5 | import xyz.gianlu.librespot.dealer.ApiClient
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class ClientTokenHandler @Inject constructor(
11 | private val spSessionManager: SpSessionManager
12 | ) {
13 | private val apiMethod = ApiClient::class.java.getDeclaredMethod("clientToken").also { it.isAccessible = true }
14 | private var sessionToken: String = ""
15 |
16 | fun requestToken() = sessionToken.ifEmpty {
17 | (apiMethod.invoke(spSessionManager.session.api()) as ClientToken.ClientTokenResponse).grantedToken.token.also {
18 | sessionToken = it
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/popcount2_external.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto2";
2 |
3 | package spotify.popcount2.proto;
4 |
5 | option optimize_for = CODE_SIZE;
6 |
7 | message PopcountRequest {
8 |
9 | }
10 |
11 | message PopcountResult {
12 | optional sint64 count = 1;
13 | optional bool truncated = 2;
14 | repeated string user = 3;
15 | repeated string userid = 6;
16 | optional int64 raw_count = 7;
17 | optional bool count_hidden_from_users = 8;
18 | }
19 |
20 | message PopcountUserUpdate {
21 | optional string user = 1;
22 | optional sint64 timestamp = 2;
23 | optional bool added = 3;
24 | optional string userid = 4;
25 | }
26 |
27 | message PopcountFollowerResult {
28 | optional bool is_truncated = 1;
29 | repeated bytes user_id = 2;
30 | }
31 |
32 | message PopcountSetFollowerCounterValueRequest {
33 | optional int64 count = 1;
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/db/LocalCollectionRepository.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.db
2 |
3 | import bruhcollective.itaysonlab.jetispot.core.collection.db.model.LocalCollectionCategory
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.withContext
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class LocalCollectionRepository @Inject constructor(
11 | private val db: LocalCollectionDatabase,
12 | private val dao: LocalCollectionDao
13 | ) {
14 | suspend fun insertOrUpdateCollection(
15 | collectionSet: String,
16 | syncToken: String
17 | ) {
18 | dao.updateCollectionCategory(LocalCollectionCategory(collectionSet, syncToken))
19 | }
20 |
21 | suspend fun clean() {
22 | withContext(Dispatchers.Default) { db.clearAllTables() }
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/api/SpPartnersApi.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.api
2 |
3 | import bruhcollective.itaysonlab.jetispot.core.objs.gql.ExtractedColors
4 | import bruhcollective.itaysonlab.jetispot.core.objs.gql.GqlWrap
5 | import retrofit2.http.GET
6 | import retrofit2.http.Query
7 |
8 | // TODO: research getExtendedMetadata EXTRACTED_METADATA value and get rid of GraphQL nonsense
9 | interface SpPartnersApi {
10 | @GET("/pathfinder/v1/query")
11 | suspend fun fetchExtractedColors(
12 | @Query("operationName") opName: String = "fetchExtractedColors",
13 | @Query("extensions") extensions: String = "{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"d7696dd106f3c84a1f3ca37225a1de292e66a2d5aced37a66632585eeb3bbbfa\"}}",
14 | @Query("variables") variables: String // variables={"uris":["{picUrl}"]}
15 | ): GqlWrap
16 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_sound_wave_outline_28.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/db/model2/CollectionTrack.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.db.model2
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity(tableName = "lcTracks")
8 | data class CollectionTrack(
9 | @PrimaryKey val id: String,
10 | val uri: String,
11 | val name: String,
12 | val albumId: String,
13 | val albumName: String,
14 | @ColumnInfo(defaultValue = "") val mainArtistName: String,
15 | val mainArtistId: String, // for metadata&joins
16 | val rawArtistsData: String, // for UI, format: ID=Name (example: 1=Artist|2=Artist2)
17 | val hasLyrics: Boolean,
18 | val isExplicit: Boolean,
19 | val duration: Int,
20 | val picture: String,
21 | @ColumnInfo(defaultValue = "") val descriptors: String, // indie|modern rock, for example
22 | val addedAt: Int
23 | )
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/CoreComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/any.proto";
4 |
5 | package com.spotify.home.dac.component.v1.proto;
6 |
7 | option java_multiple_files = true;
8 | option java_package = "com.spotify.home.dac.component.v1.proto";
9 |
10 | message HomePageComponent {
11 | repeated google.protobuf.Any components = 2;
12 | }
13 |
14 | message SectionHeaderComponent {
15 | string title = 1;
16 | }
17 |
18 | message SectionComponent {
19 | repeated google.protobuf.Any components = 2;
20 | }
21 |
22 | message RecsplanationHeadingComponent {
23 | string title = 1;
24 | string subtitle = 2;
25 | string navigate_uri = 3;
26 | string image_uri = 4;
27 | }
28 |
29 | // only ubi(soft?!) data here, recent played data are fetched separately
30 | message RecentlyPlayedSectionComponent {}
31 |
32 | message SnappyGridSectionComponent {
33 | repeated google.protobuf.Any components = 1;
34 | int32 rows = 2;
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/playback/helpers/MediaItemWrapper.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.playback.helpers
2 |
3 | import androidx.compose.ui.graphics.asImageBitmap
4 | import androidx.media2.common.MediaItem
5 | import androidx.media2.common.MediaMetadata
6 |
7 | class MediaItemWrapper(
8 | private val item: MediaItem? = null
9 | ) {
10 | val hasMetadata get() = item != null
11 |
12 | val title get() = item?.metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) ?: "Unknown Title"
13 | val artist get() = item?.metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) ?: "Unknown Artist"
14 | val album get() = item?.metadata?.getString(MediaMetadata.METADATA_KEY_ALBUM) ?: "Unknown Album"
15 | val duration get() = item?.metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L
16 |
17 | val artwork get() = item?.metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART)
18 | val artworkCompose by lazy { artwork?.asImageBitmap() }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/ext/ColorExt.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.ext
2 |
3 | import androidx.annotation.FloatRange
4 | import androidx.compose.material3.ColorScheme
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.graphics.compositeOver
7 | import androidx.compose.ui.unit.Dp
8 | import androidx.compose.ui.unit.dp
9 | import kotlin.math.ln
10 |
11 | fun ColorScheme.compositeSurfaceElevation(
12 | elevation: Dp,
13 | ): Color {
14 | if (elevation == 0.dp) return surface
15 | val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
16 | return surfaceTint.copy(alpha = alpha).compositeOver(surface)
17 | }
18 |
19 | fun Color.blendWith(color: Color, @FloatRange(from = 0.0, to = 1.0) ratio: Float): Color {
20 | val inv = 1f - ratio
21 | return copy(
22 | red = red * inv + color.red * ratio,
23 | blue = blue * inv + color.blue * ratio,
24 | green = green * inv + color.green * ratio,
25 | )
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/Revision.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.util
2 |
3 | import android.util.Base64
4 | import com.google.protobuf.ByteString
5 | import java.nio.ByteBuffer
6 |
7 | object Revision {
8 | fun base64ToRevision(b64: String) = Base64.decode(b64, Base64.DEFAULT).let { b64dec ->
9 | val buffer = ByteBuffer.wrap(b64dec)
10 |
11 | val revId = buffer.int
12 | val revHash = ByteArray(buffer.remaining()).also { buffer.get(it) }.joinToString("") {
13 | (0xFF and it.toInt()).toString(16).padStart(2, '0')
14 | }.padEnd(40, '0')
15 |
16 | return@let "$revId,$revHash"
17 | }
18 |
19 | fun byteStringToRevision(bs: ByteString): String {
20 | val buffer = bs.asReadOnlyByteBuffer()
21 |
22 | val revId = buffer.int
23 | val revHash = ByteArray(buffer.remaining()).also { buffer.get(it) }.joinToString("") {
24 | (0xFF and it.toInt()).toString(16).padStart(2, '0')
25 | }.padEnd(40, '0')
26 |
27 | return "$revId,$revHash"
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/api/SpCollectionApi.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.api
2 |
3 | import com.spotify.collection2.v2.proto.Collection2V2
4 | import retrofit2.http.Body
5 | import retrofit2.http.Header
6 | import retrofit2.http.Headers
7 | import retrofit2.http.POST
8 |
9 | interface SpCollectionApi {
10 | @POST("write")
11 | @Headers("Accept: application/vnd.collection-v2.spotify.proto", "Content-Type: application/vnd.collection-v2.spotify.proto")
12 | suspend fun write(@Body data: Collection2V2.WriteRequest)
13 |
14 | @POST("delta")
15 | @Headers("Accept: application/vnd.collection-v2.spotify.proto", "Content-Type: application/vnd.collection-v2.spotify.proto")
16 | suspend fun delta(@Body data: Collection2V2.DeltaRequest): Collection2V2.DeltaResponse
17 |
18 | @POST("paging")
19 | @Headers("Accept: application/vnd.collection-v2.spotify.proto", "Content-Type: application/vnd.collection-v2.spotify.proto")
20 | suspend fun paging(@Body data: Collection2V2.PageRequest): Collection2V2.PageResponse
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_home/SnappyGridSectionComponentBinder.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.dac.components_home
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.height
5 | import androidx.compose.foundation.lazy.grid.GridCells
6 | import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
7 | import androidx.compose.foundation.lazy.grid.items
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.unit.dp
11 | import bruhcollective.itaysonlab.jetispot.ui.dac.DacRender
12 | import com.spotify.home.dac.component.v1.proto.SnappyGridSectionComponent
13 |
14 | @Composable
15 | fun SnappyGridSectionComponentBinder(
16 | item: SnappyGridSectionComponent
17 | ) {
18 | LazyHorizontalGrid(rows = GridCells.Fixed(item.componentsCount), Modifier.fillMaxWidth().height(56.dp * item.componentsCount)) {
19 | items(item.componentsList) { cItem ->
20 | DacRender(item = item)
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/dynamic_blocks/DynamicPlayButton.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.shared.dynamic_blocks
2 |
3 | import androidx.compose.foundation.shape.CircleShape
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.rounded.PlayArrow
6 | import androidx.compose.material3.FilledIconButton
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.draw.clip
11 | import bruhcollective.itaysonlab.jetispot.ui.dac.LocalDacDelegate
12 | import com.spotify.dac.player.v1.proto.PlayCommand
13 |
14 | @Composable
15 | fun DynamicPlayButton(
16 | command: PlayCommand,
17 | modifier: Modifier = Modifier
18 | ) {
19 | val dacDelegate = LocalDacDelegate.current
20 | FilledIconButton(
21 | onClick = { dacDelegate.dispatchPlay(command) }, modifier = modifier.clip(CircleShape)
22 | ) {
23 | Icon(Icons.Rounded.PlayArrow, contentDescription = null)
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
16 |
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/HubScreenDelegate.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.compose.runtime.State
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.staticCompositionLocalOf
7 | import androidx.compose.ui.graphics.Color
8 | import bruhcollective.itaysonlab.jetispot.core.objs.player.PlayFromContextData
9 | import kotlinx.coroutines.CoroutineScope
10 |
11 | @Stable
12 | interface HubScreenDelegate {
13 | fun play(data: PlayFromContextData)
14 | fun isSurroundedWithPadding(): Boolean = false
15 | // headers
16 | suspend fun calculateDominantColor(url: String, dark: Boolean): Color = Color.Transparent
17 | suspend fun getLikedSongsCount(artistId: String): Int = 0
18 | // states
19 | fun getMainObjectAddedState(): State = mutableStateOf(false)
20 | fun toggleMainObjectAddedState() {}
21 | fun sendCustomCommand(scope: CoroutineScope, cmd: Any): Any = Unit
22 | }
23 |
24 | val LocalHubScreenDelegate = staticCompositionLocalOf { error("HubScreenDelegate should be initialized") }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/SpApp.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot
2 |
3 | import android.app.Application
4 | import android.os.Build
5 | import bruhcollective.itaysonlab.jetispot.playback.sp.AndroidNativeDecoder
6 | import com.tencent.mmkv.MMKV
7 | import dagger.hilt.android.HiltAndroidApp
8 | import org.slf4j.LoggerFactory
9 | import org.slf4j.impl.HandroidLoggerAdapter
10 | import xyz.gianlu.librespot.audio.decoders.Decoders
11 | import xyz.gianlu.librespot.audio.format.SuperAudioFormat
12 | import xyz.gianlu.librespot.player.state.DeviceStateHandler
13 |
14 | @HiltAndroidApp
15 | class SpApp: Application() {
16 | init {
17 | Decoders.registerDecoder(SuperAudioFormat.VORBIS, AndroidNativeDecoder::class.java)
18 | Decoders.registerDecoder(SuperAudioFormat.MP3, AndroidNativeDecoder::class.java)
19 |
20 | HandroidLoggerAdapter.DEBUG = BuildConfig.DEBUG
21 | HandroidLoggerAdapter.ANDROID_API_LEVEL = Build.VERSION.SDK_INT
22 | HandroidLoggerAdapter.APP_NAME = "SpApp"
23 | }
24 |
25 | override fun onCreate() {
26 | super.onCreate()
27 | MMKV.initialize(this, "${filesDir.absolutePath}/spa_meta")
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/identity.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package spotify.identity.v3;
4 |
5 | import "google/protobuf/field_mask.proto";
6 | import "google/protobuf/wrappers.proto";
7 |
8 | option optimize_for = CODE_SIZE;
9 | option java_outer_classname = "IdentityV3";
10 | option java_package = "com.spotify.identity.proto.v3";
11 |
12 | message Image {
13 | int32 max_width = 1;
14 | int32 max_height = 2;
15 | string url = 3;
16 | }
17 |
18 | message UserProfile {
19 | google.protobuf.StringValue username = 1;
20 | google.protobuf.StringValue name = 2;
21 | repeated Image images = 3;
22 | google.protobuf.BoolValue verified = 4;
23 | google.protobuf.BoolValue edit_profile_disabled = 5;
24 | google.protobuf.BoolValue report_abuse_disabled = 6;
25 | google.protobuf.BoolValue abuse_reported_name = 7;
26 | google.protobuf.BoolValue abuse_reported_image = 8;
27 | google.protobuf.BoolValue has_spotify_name = 9;
28 | google.protobuf.BoolValue has_spotify_image = 10;
29 | google.protobuf.Int32Value color = 11;
30 | }
31 |
32 | message UserProfileUpdateRequest {
33 | google.protobuf.FieldMask mask = 1;
34 | UserProfile user_profile = 2;
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/ext/ContextExt.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.ext
2 |
3 | import android.app.Activity
4 | import android.content.*
5 | import androidx.core.content.getSystemService
6 | import androidx.core.net.toUri
7 | import androidx.navigation.NavController
8 |
9 | fun NavController.openUrl(url: String) = context.openUrl(url)
10 | fun Context.openUrl(url: String) = startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
11 |
12 | fun Context.shareUrl(url: String, title: String? = null) = startActivity(Intent(Intent.ACTION_SEND).apply {
13 | type = "text/plain"
14 | putExtra(Intent.EXTRA_TEXT, url)
15 | putExtra(Intent.EXTRA_TITLE, title)
16 | }.let { Intent.createChooser(it, null) })
17 |
18 | fun Context.copy(txt: String) {
19 | getSystemService()?.setPrimaryClip(ClipData.newPlainText("jetispot", txt))
20 | }
21 |
22 | // Shamelessly taken from Google
23 | internal fun Context.findActivity(): Activity {
24 | var context = this
25 | while (context is ContextWrapper) {
26 | if (context is Activity) return context
27 | context = context.baseContext
28 | }
29 | throw IllegalStateException("Not an Activity context!")
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/HomeSectionHeader.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.Text
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.text.font.FontWeight
11 | import androidx.compose.ui.unit.dp
12 | import androidx.compose.ui.unit.sp
13 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubText
14 | import bruhcollective.itaysonlab.jetispot.ui.hub.HubScreenDelegate
15 | import bruhcollective.itaysonlab.jetispot.ui.hub.LocalHubScreenDelegate
16 |
17 | @Composable
18 | fun HomeSectionHeader (
19 | text: HubText,
20 | ) {
21 | Box(Modifier.padding(vertical = 8.dp).padding(horizontal = if (LocalHubScreenDelegate.current.isSurroundedWithPadding()) 0.dp else 16.dp)) {
22 | Text(text = text.title!!, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 21.sp, modifier = Modifier.align(Alignment.CenterStart))
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/lyrics/SpLyricsRequester.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.lyrics
2 |
3 | import bruhcollective.itaysonlab.jetispot.core.api.SpColorLyricsApi
4 | import bruhcollective.itaysonlab.jetispot.core.metadata_db.SpMetadataDb
5 | import com.spotify.lyrics.v2.lyrics.proto.ColorLyricsResponse
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.withContext
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | @Singleton
12 | class SpLyricsRequester @Inject constructor(
13 | private val spMetadataDb: SpMetadataDb,
14 | private val spColorLyricsApi: SpColorLyricsApi
15 | ) {
16 | suspend fun request(track: String) = withContext(Dispatchers.IO) {
17 | val uri = "lyrics:$track"
18 |
19 | if (spMetadataDb.contains(uri)) {
20 | return@withContext ColorLyricsResponse.parseFrom(spMetadataDb.get(uri))
21 | } else {
22 | val response = spColorLyricsApi.getLyrics(spotifyId = track)
23 |
24 | if (response.hasLyrics()) {
25 | spMetadataDb.put(uri, response.toByteArray())
26 | return@withContext response
27 | } else {
28 | return@withContext null
29 | }
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/ext/FlowExt.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.ext
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.State
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.ui.platform.LocalLifecycleOwner
8 | import androidx.lifecycle.Lifecycle
9 | import androidx.lifecycle.LifecycleOwner
10 | import androidx.lifecycle.flowWithLifecycle
11 | import kotlinx.coroutines.flow.Flow
12 | import kotlin.coroutines.CoroutineContext
13 | import kotlin.coroutines.EmptyCoroutineContext
14 |
15 | @Composable
16 | fun rememberFlow(
17 | flow: Flow,
18 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
19 | ): Flow {
20 | return remember(key1 = flow, key2 = lifecycleOwner) { flow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) }
21 | }
22 |
23 | @Composable
24 | fun Flow.collectAsStateLifecycleAware(
25 | initial: R,
26 | context: CoroutineContext = EmptyCoroutineContext
27 | ): State {
28 | val lifecycleAwareFlow = rememberFlow(flow = this)
29 | return lifecycleAwareFlow.collectAsState(initial = initial, context = context)
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/playback/service/refl/SpReflect.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.playback.service.refl
2 |
3 | import androidx.media2.common.SessionPlayer
4 | import com.google.gson.JsonParser
5 | import xyz.gianlu.librespot.player.PagesLoader
6 | import xyz.gianlu.librespot.player.Player
7 |
8 | class SpReflect(
9 | private val player: () -> Player
10 | ) {
11 | fun playUsingData (data: String) {
12 | try {
13 | player().callPlayFromObj(JsonParser.parseString(data).asJsonObject)
14 | } catch (e: Exception) {
15 | e.printStackTrace()
16 | }
17 | }
18 |
19 | @SessionPlayer.RepeatMode
20 | fun getRepeatMode(): Int {
21 | val options = stateOf(player()).options
22 | return when {
23 | options.repeatingContext -> SessionPlayer.REPEAT_MODE_ALL
24 | options.repeatingTrack -> SessionPlayer.REPEAT_MODE_ONE
25 | else -> SessionPlayer.REPEAT_MODE_NONE
26 | }
27 | }
28 |
29 | @SessionPlayer.ShuffleMode
30 | fun getShuffleMode(): Int {
31 | return if (stateOf(player()).options.shufflingContext) SessionPlayer.SHUFFLE_MODE_ALL else SessionPlayer.SHUFFLE_MODE_NONE
32 | }
33 |
34 | private fun stateOf(player: Player) = player.stateWrapper.state
35 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/PromoComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/any.proto";
4 | import "sp/DacPlayer.proto";
5 |
6 | package com.spotify.home.dac.component.v1.proto;
7 |
8 | option java_multiple_files = true;
9 | option java_package = "com.spotify.home.dac.component.v1.proto";
10 |
11 | message PromoSectionHeadingComponent {
12 | string title = 1;
13 | google.protobuf.Any context_menu = 2;
14 | }
15 |
16 | message AlbumCardPromoComponent {
17 | string title = 1;
18 | string subtitle = 2;
19 | string navigate_uri = 3;
20 | string like_uri = 4;
21 | string image_uri = 5;
22 | string tag = 6;
23 | spotify.dac.player.v1.proto.PlayCommand play_command = 7;
24 | }
25 |
26 | message PromoCardOnlyYouComponent {
27 | string title = 1;
28 | string subtitle = 2;
29 | string navigate_uri = 3;
30 | string background_image_uri = 4;
31 | }
32 |
33 | message PromoCardHomeComponent {
34 | string title = 1;
35 | string subtitle = 2;
36 | string tag = 3;
37 | string navigate_uri = 4;
38 | string background_image_uri = 5;
39 | string logo_image_uri = 6;
40 | string gradient_color = 7;
41 | spotify.dac.player.v1.proto.PlayCommand play_command = 8;
42 | google.protobuf.Any context_menu = 9;
43 | string play_button_color = 10;
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/SpUtils.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.util
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.provider.Settings
6 | import com.google.protobuf.ByteString
7 | import xyz.gianlu.librespot.common.Utils
8 | import xyz.gianlu.librespot.metadata.ImageId
9 |
10 | object SpUtils {
11 | const val SPOTIFY_APP_VERSION = "8.7.68.568"
12 |
13 | fun getDeviceName(appContext: Context): String {
14 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
15 | val deviceName = Settings.Global.getString(appContext.contentResolver, Settings.Global.DEVICE_NAME)
16 | if (deviceName == Build.MODEL) Build.MODEL else "$deviceName (${Build.MODEL})"
17 | } else {
18 | Build.MODEL
19 | }
20 | }
21 |
22 | fun getRandomString(length: Int) : String {
23 | val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
24 | return (1..length).map { allowedChars.random() }.joinToString("")
25 | }
26 |
27 | fun getScannableUrl(uri: String) = "https://scannables.scdn.co/uri/800/$uri"
28 | fun getImageUrl(bytes: ByteString?) = if (bytes != null) "https://i.scdn.co/image/${Utils.bytesToHex(bytes).lowercase()}" else null
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/AlbumTrackRow.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.text.font.FontWeight
9 | import androidx.compose.ui.unit.dp
10 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
11 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
12 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
13 | import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext
14 |
15 | @Composable
16 | fun AlbumTrackRow(
17 | item: HubItem
18 | ) {
19 | Column(Modifier.clickableHub(item).fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)) {
20 | var drawnTitle = false
21 |
22 | if (!item.text?.title.isNullOrEmpty()) {
23 | drawnTitle = true
24 | MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal)
25 | }
26 |
27 | if (!item.text?.subtitle.isNullOrEmpty()) {
28 | Subtext(item.text!!.subtitle!!, modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp))
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/HubEventHandler.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.compose.ui.Modifier
5 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubEvent
6 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
7 | import bruhcollective.itaysonlab.jetispot.ui.navigation.NavigationController
8 | import bruhcollective.itaysonlab.jetispot.ui.shared.navAndHubClickable
9 | import bruhcollective.itaysonlab.jetispot.ui.shared.navClickable
10 |
11 | object HubEventHandler {
12 | fun handle (navController: NavigationController, delegate: HubScreenDelegate, event: HubEvent) {
13 | when (event) {
14 | is HubEvent.NavigateToUri -> {
15 | if (event.data.uri.startsWith("http")) {
16 | navController.openInBrowser(event.data.uri)
17 | } else {
18 | navController.navigate(event.data.uri)
19 | }
20 | }
21 | is HubEvent.PlayFromContext -> delegate.play(event.data)
22 | HubEvent.Unknown -> {}
23 | }
24 | }
25 | }
26 |
27 | @Stable
28 | fun Modifier.clickableHub(item: HubItem) = navAndHubClickable(enabled = item.events?.click != null) { navController, delegate ->
29 | HubEventHandler.handle(navController, delegate, item.events!!.click!!)
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/SectionHeader.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.Text
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.text.font.FontWeight
11 | import androidx.compose.ui.unit.dp
12 | import androidx.compose.ui.unit.sp
13 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubText
14 | import bruhcollective.itaysonlab.jetispot.ui.hub.LocalHubScreenDelegate
15 |
16 | @Composable
17 | fun SectionHeader(
18 | text: HubText,
19 | ) {
20 | Box(
21 | Modifier
22 | .padding(
23 | vertical = 8.dp,
24 | horizontal = if (LocalHubScreenDelegate.current.isSurroundedWithPadding()) 0.dp else 16.dp
25 | )
26 | ) {
27 | Text(
28 | text = text.title!!,
29 | color = MaterialTheme.colorScheme.onSurface,
30 | fontWeight = FontWeight.Bold,
31 | fontSize = 16.sp,
32 | modifier = Modifier.align(Alignment.CenterStart)
33 | )
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/monet/google/utils/StringUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package bruhcollective.itaysonlab.jetispot.ui.monet.google.utils;
18 |
19 | /** Utility methods for string representations of colors. */
20 | final class StringUtils {
21 | private StringUtils() {}
22 |
23 | /**
24 | * Hex string representing color, ex. #ff0000 for red.
25 | *
26 | * @param argb ARGB representation of a color.
27 | */
28 | public static String hexFromArgb(int argb) {
29 | int red = ColorUtils.redFromArgb(argb);
30 | int blue = ColorUtils.blueFromArgb(argb);
31 | int green = ColorUtils.greenFromArgb(argb);
32 | return String.format("#%02x%02x%02x", red, green, blue);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/Device.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.util
2 |
3 | import android.app.usage.StorageStatsManager
4 | import android.content.Context
5 | import android.os.Build
6 | import android.os.Environment
7 | import android.os.StatFs
8 | import android.os.storage.StorageManager
9 | import androidx.annotation.RequiresApi
10 | import androidx.core.content.getSystemService
11 |
12 | object Device {
13 | fun getInternalStorageSize(context: Context): StorageSize = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
14 | context.getSystemService()?.let { modernImpl(it) } ?: legacyImpl()
15 | } else {
16 | legacyImpl()
17 | }
18 |
19 | @RequiresApi(Build.VERSION_CODES.O)
20 | private fun modernImpl(manager: StorageStatsManager) = StorageSize(manager.getFreeBytes(StorageManager.UUID_DEFAULT) to manager.getTotalBytes(StorageManager.UUID_DEFAULT))
21 |
22 | private fun statFs() = StatFs(Environment.getDataDirectory().path)
23 | private fun legacyImpl() = statFs().let { fs -> StorageSize(fs.availableBlocksLong * fs.blockSizeLong to fs.blockCountLong * fs.blockSizeLong) }
24 |
25 | @JvmInline
26 | value class StorageSize(private val src: Pair) {
27 | val free get() = src.first
28 | val total get() = src.second
29 | val taken get() = total - free
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ImageRow.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.shape.CircleShape
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.draw.clip
9 | import androidx.compose.ui.text.font.FontWeight
10 | import androidx.compose.ui.unit.dp
11 | import androidx.compose.ui.unit.sp
12 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
13 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
14 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
15 | import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage
16 |
17 | @Composable
18 | fun ImageRow(
19 | item: HubItem
20 | ) {
21 | Row(Modifier.clickableHub(item).fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)) {
22 | PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier
23 | .size(42.dp)
24 | .clip(CircleShape))
25 |
26 | Column(Modifier.padding(horizontal = 12.dp).align(Alignment.CenterVertically)) {
27 | MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal, fontSize = 18.sp)
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpSessionManager.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core
2 |
3 | import android.content.Context
4 | import bruhcollective.itaysonlab.jetispot.core.util.SpUtils
5 | import com.spotify.connectstate.Connect
6 | import dagger.hilt.android.qualifiers.ApplicationContext
7 | import xyz.gianlu.librespot.core.Session
8 | import java.io.File
9 | import java.util.*
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | @Suppress("BlockingMethodInNonBlockingContext")
14 | @Singleton
15 | class SpSessionManager @Inject constructor(
16 | @ApplicationContext val appContext: Context,
17 | ) {
18 | private var _session: Session? = null
19 | val session get() = _session ?: throw IllegalStateException("Session is not created yet!")
20 |
21 | fun createSession(): Session.Builder = Session.Builder(createCfg()).setDeviceType(Connect.DeviceType.SMARTPHONE).setDeviceName(
22 | SpUtils.getDeviceName(appContext)).setDeviceId(null).setPreferredLocale(Locale.getDefault().language)
23 | private fun createCfg() = Session.Configuration.Builder().setCacheEnabled(true).setDoCacheCleanUp(true).setCacheDir(File(appContext.cacheDir, "spa_cache")).setStoredCredentialsFile(File(appContext.filesDir, "spa_creds")).build()
24 |
25 | fun isSignedIn() = _session?.isValid == true
26 | fun setSession(s: Session) { _session = s }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/FindCard.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.Card
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.text.font.FontWeight
11 | import androidx.compose.ui.text.style.TextOverflow
12 | import androidx.compose.ui.unit.dp
13 | import androidx.compose.ui.unit.sp
14 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
15 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
16 | import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage
17 |
18 | @Composable
19 | fun FindCard(
20 | item: HubItem
21 | ) {
22 | Card(modifier = Modifier.height(100.dp).fillMaxWidth().clickableHub(item)) {
23 | Box {
24 | PreviewableAsyncImage(imageUrl = item.images?.background?.uri, placeholderType = item.images?.background?.placeholder, modifier = Modifier.fillMaxSize())
25 | 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))
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/OutlineButton.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.material.Icon
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.rounded.ChevronRight
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.unit.dp
15 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
16 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
17 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
18 |
19 | @Composable
20 | fun OutlineButton(
21 | item: HubItem
22 | ) {
23 | Box(Modifier.clickableHub(item).padding(16.dp)) {
24 | Row(Modifier.align(Alignment.Center)) {
25 | MediumText(text = item.text?.title!!, modifier = Modifier.align(Alignment.CenterVertically))
26 | Icon(Icons.Rounded.ChevronRight, null, tint = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(start = 2.dp).size(20.dp).align(Alignment.CenterVertically))
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/di/CollectionModule.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.di
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import bruhcollective.itaysonlab.jetispot.core.SpMetadataRequester
6 | import bruhcollective.itaysonlab.jetispot.core.SpSessionManager
7 | import bruhcollective.itaysonlab.jetispot.core.api.SpCollectionApi
8 | import bruhcollective.itaysonlab.jetispot.core.api.SpInternalApi
9 | import bruhcollective.itaysonlab.jetispot.core.collection.SpCollectionManager
10 | import bruhcollective.itaysonlab.jetispot.core.collection.db.LocalCollectionDao
11 | import bruhcollective.itaysonlab.jetispot.core.collection.db.LocalCollectionDatabase
12 | import bruhcollective.itaysonlab.jetispot.core.collection.db.LocalCollectionRepository
13 | import dagger.Module
14 | import dagger.Provides
15 | import dagger.hilt.InstallIn
16 | import dagger.hilt.android.qualifiers.ApplicationContext
17 | import dagger.hilt.components.SingletonComponent
18 |
19 | @Module
20 | @InstallIn(SingletonComponent::class)
21 | object CollectionModule {
22 | @Provides
23 | fun provideDatabase (
24 | @ApplicationContext appCtx: Context
25 | ): LocalCollectionDatabase = Room.databaseBuilder(appCtx, LocalCollectionDatabase::class.java, "spCollection").build()
26 |
27 | @Provides
28 | fun provideDao (
29 | db: LocalCollectionDatabase
30 | ): LocalCollectionDao = db.dao()
31 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/collection/collection2v2.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package spotify.collection.proto.v2;
4 |
5 | option optimize_for = CODE_SIZE;
6 | option java_package = "com.spotify.collection2.v2.proto";
7 |
8 | message PageRequest {
9 | string username = 1;
10 | string set = 2;
11 | string pagination_token = 3;
12 | int32 limit = 4;
13 | }
14 |
15 | message CollectionItem {
16 | string uri = 1;
17 | int32 added_at = 2;
18 | bool is_removed = 3;
19 | }
20 |
21 | message PageResponse {
22 | repeated CollectionItem items = 1;
23 | string next_page_token = 2;
24 | string sync_token = 3;
25 | }
26 |
27 | message DeltaRequest {
28 | string username = 1;
29 | string set = 2;
30 | string last_sync_token = 3;
31 | }
32 |
33 | message DeltaResponse {
34 | bool delta_update_possible = 1;
35 | repeated CollectionItem items = 2;
36 | string sync_token = 3;
37 | }
38 |
39 | message WriteRequest {
40 | string username = 1;
41 | string set = 2;
42 | repeated CollectionItem items = 3;
43 | string client_update_id = 4;
44 | }
45 |
46 | message PubSubUpdate {
47 | string username = 1;
48 | string set = 2;
49 | repeated CollectionItem items = 3;
50 | string client_update_id = 4;
51 | }
52 |
53 | message InitializedRequest {
54 | string username = 1;
55 | string set = 2;
56 | }
57 |
58 | message InitializedResponse {
59 | bool initialized = 1;
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/player/PlayFromContextData.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.objs.player
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | class PlayFromContextData (
7 | val uri: String,
8 | val player: PlayFromContextPlayerData,
9 | )
10 |
11 | @JsonClass(generateAdapter = true)
12 | class PlayFromContextPlayerData (
13 | val context: PfcContextData? = null,
14 | val options: PfcOptions? = null,
15 | val state: PfcState? = null,
16 | val play_origin: Map = mapOf(),
17 | )
18 |
19 | @JsonClass(generateAdapter = true)
20 | class PfcContextData (
21 | val url: String? = null,
22 | val uri: String,
23 | val metadata: PfcContextMetadata? = null
24 | )
25 |
26 | @JsonClass(generateAdapter = true)
27 | class PfcOptions (
28 | val skip_to: PfcOptSkipTo? = null,
29 | val player_options_override: PfcStateOptions? = null
30 | )
31 |
32 | @JsonClass(generateAdapter = true)
33 | class PfcContextMetadata (
34 | val context_description: String? = null,
35 | )
36 |
37 | @JsonClass(generateAdapter = true)
38 | class PfcOptSkipTo (
39 | val page_index: Int? = null,
40 | val track_uri: String
41 | )
42 |
43 | @JsonClass(generateAdapter = true)
44 | class PfcState (
45 | val options: PfcStateOptions? = null
46 | )
47 |
48 | @JsonClass(generateAdapter = true)
49 | class PfcStateOptions (
50 | val shuffling_context: Boolean = false
51 | )
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/LargerRow.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.text.font.FontWeight
11 | import androidx.compose.ui.unit.dp
12 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
13 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
14 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
15 | import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage
16 | import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext
17 |
18 | @Composable
19 | fun LargerRow (
20 | item: HubItem
21 | ) {
22 | Row(
23 | Modifier
24 | .clickableHub(item)
25 | .padding(horizontal = 16.dp, vertical = 2.dp)) {
26 | PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier.size(72.dp).padding(end = 16.dp).padding(vertical = 8.dp))
27 | Column(Modifier.align(Alignment.CenterVertically)) {
28 | MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal, modifier = Modifier.padding(bottom = 4.dp))
29 | Subtext(item.text.subtitle!!)
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/ShortcutsComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/any.proto";
4 | import "sp/DacPlayer.proto";
5 |
6 | package com.spotify.home.dac.component.v1.proto;
7 |
8 | option java_multiple_files = true;
9 | option java_package = "com.spotify.home.dac.component.v1.proto";
10 |
11 | message ShortcutsSectionComponent {
12 | string title = 1;
13 | repeated google.protobuf.Any shortcuts = 2;
14 | }
15 |
16 | message AlbumCardShortcutComponent {
17 | string title = 1;
18 | string navigate_uri = 2;
19 | string image_uri = 3;
20 | com.spotify.dac.player.v1.proto.PlayCommand play_command = 4;
21 | }
22 |
23 | message PlaylistCardShortcutComponent {
24 | string title = 1;
25 | string navigate_uri = 2;
26 | string image_uri = 3;
27 | com.spotify.dac.player.v1.proto.PlayCommand play_command = 4;
28 | }
29 |
30 | message ShowCardShortcutComponent {
31 | string title = 1;
32 | string navigate_uri = 2;
33 | string image_uri = 3;
34 | com.spotify.dac.player.v1.proto.PlayCommand play_command = 4;
35 | }
36 |
37 | message ArtistCardShortcutComponent {
38 | string title = 1;
39 | string navigate_uri = 2;
40 | string image_uri = 3;
41 | com.spotify.dac.player.v1.proto.PlayCommand play_command = 4;
42 | }
43 |
44 | message EpisodeCardShortcutComponent {
45 | string title = 1;
46 | string navigate_uri = 2;
47 | string image_uri = 3;
48 | bool fresh = 4;
49 | int64 progress_percentage = 5;
50 | com.spotify.dac.player.v1.proto.PlayCommand play_command = 6;
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ArtistPinnedItem.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.text.font.FontWeight
11 | import androidx.compose.ui.unit.dp
12 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
13 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
14 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
15 | import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage
16 | import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext
17 |
18 | @Composable
19 | fun ArtistPinnedItem (
20 | item: HubItem
21 | ) {
22 | Row(
23 | Modifier
24 | .clickableHub(item)
25 | .padding(horizontal = 16.dp, vertical = 2.dp)) {
26 | PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier.size(72.dp).padding(vertical = 8.dp).padding(end = 16.dp))
27 | Column(Modifier.align(Alignment.CenterVertically)) {
28 | MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal, modifier = Modifier.padding(bottom = 4.dp))
29 | Subtext(item.text.subtitle!!)
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp_ext/searchview.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_package = "bruhcollective.itaysonlab.jetispot.proto";
4 | option java_multiple_files = true;
5 |
6 | message SearchViewResponse {
7 | repeated SearchEntity hits = 1;
8 | }
9 |
10 | message SearchEntity {
11 | string uri = 1;
12 | string name = 2;
13 | string image_uri = 3;
14 |
15 | oneof entity {
16 | Artist artist = 4;
17 | Track track = 5;
18 | Album album = 6;
19 | Playlist playlist = 7;
20 | }
21 |
22 | message Artist {
23 | bool verified = 1;
24 | }
25 |
26 | message Track {
27 | message OnDemand {
28 | string trackUri = 1;
29 | string playlistUri = 2;
30 | }
31 |
32 | message RelatedEntity {
33 | string uri = 1;
34 | string name = 2;
35 | }
36 |
37 | bool explicit = 1;
38 | bool windowed = 2;
39 | RelatedEntity trackAlbum = 3;
40 | repeated RelatedEntity trackArtists = 4;
41 | optional string previewId = 5;
42 | optional bool mogef19 = 6;
43 | optional bool lyricsMatch = 7;
44 | optional OnDemand onDemand = 8;
45 | }
46 |
47 | message Album {
48 | enum Type {
49 | UNDEFINED = 0;
50 | ALBUM = 1;
51 | SINGLE = 2;
52 | COMPILATION = 3;
53 | EP = 4;
54 | AUDIOBOOK = 5;
55 | PODCAST = 6;
56 | }
57 |
58 | repeated string artistNames = 1;
59 | Type type = 2;
60 | int32 releaseDate = 3;
61 | }
62 |
63 | message Playlist {
64 | bool personalized = 1;
65 | bool ownedBySpotify = 2;
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/blocks/TwoColumnAndImageBlock.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.blocks
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.text.font.FontWeight
9 | import androidx.compose.ui.unit.dp
10 | import androidx.compose.ui.unit.sp
11 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
12 | import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage
13 | import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext
14 | import xyz.gianlu.librespot.metadata.ImageId
15 |
16 | @Composable
17 | fun TwoColumnAndImageBlock(
18 | artworkUri: String?,
19 | title: String,
20 | text: String,
21 | modifier: Modifier = Modifier
22 | ) {
23 | Row(modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)) {
24 | PreviewableAsyncImage(imageUrl = remember(artworkUri) {
25 | artworkUri?.let { "https://i.scdn.co/image/" + ImageId.fromUri(it).hexId() }
26 | }, placeholderType = "track", modifier = Modifier
27 | .size(48.dp))
28 |
29 | Column(Modifier.padding(horizontal = 12.dp).align(Alignment.CenterVertically)) {
30 | MediumText(title, fontWeight = FontWeight.Normal, fontSize = 18.sp)
31 | Spacer(Modifier.height(4.dp))
32 | Subtext(text, modifier = Modifier, maxLines = 1)
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/collection/db/LocalCollectionDatabase.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.collection.db
2 |
3 | import androidx.room.AutoMigration
4 | import androidx.room.Database
5 | import androidx.room.DeleteTable
6 | import androidx.room.RoomDatabase
7 | import androidx.room.migration.AutoMigrationSpec
8 | import bruhcollective.itaysonlab.jetispot.core.collection.db.model.LocalCollectionCategory
9 | import bruhcollective.itaysonlab.jetispot.core.collection.db.model2.*
10 | import bruhcollective.itaysonlab.jetispot.core.collection.db.model2.rootlist.CollectionRootlistItem
11 |
12 | @Database(
13 | entities = [
14 | LocalCollectionCategory::class,
15 | CollectionArtist::class,
16 | CollectionAlbum::class,
17 | CollectionTrack::class,
18 | CollectionRootlistItem::class,
19 | CollectionContentFilter::class,
20 | CollectionPinnedItem::class,
21 | CollectionShow::class,
22 | CollectionEpisode::class,
23 | ], version = 7, autoMigrations = [
24 | AutoMigration(from = 1, to = 2),
25 | AutoMigration(from = 2, to = 3),
26 | AutoMigration(from = 3, to = 4),
27 | AutoMigration(from = 4, to = 5, spec = LocalCollectionDatabase.RemoveArtistsMeta::class),
28 | AutoMigration(from = 5, to = 6),
29 | AutoMigration(from = 6, to = 7),
30 | ], exportSchema = true
31 | )
32 | abstract class LocalCollectionDatabase : RoomDatabase() {
33 | abstract fun dao(): LocalCollectionDao
34 |
35 | @DeleteTable(tableName = "lcMetaArtists")
36 | class RemoveArtistsMeta: AutoMigrationSpec
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ShortcutsCard.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.Card
5 | import androidx.compose.material3.CardDefaults
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.text.style.TextOverflow
12 | import androidx.compose.ui.unit.dp
13 | import androidx.compose.ui.unit.sp
14 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
15 | import bruhcollective.itaysonlab.jetispot.ui.ext.compositeSurfaceElevation
16 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
17 | import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage
18 |
19 | @Composable
20 | fun ShortcutsCard(
21 | item: HubItem
22 | ) {
23 | Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.compositeSurfaceElevation(3.dp)), modifier = Modifier.height(56.dp).fillMaxWidth()) {
24 | Row(Modifier.clickableHub(item)) {
25 | PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier.size(56.dp))
26 | Text(item.text!!.title!!, fontSize = 13.sp, lineHeight = 18.sp, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.align(Alignment.CenterVertically).padding(horizontal = 8.dp))
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_home/FilterComponentBinder.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.dac.components_home
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.foundation.lazy.LazyRow
6 | import androidx.compose.foundation.lazy.items
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.rounded.Check
9 | import androidx.compose.material3.ExperimentalMaterial3Api
10 | import androidx.compose.material3.FilterChip
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.unit.dp
16 | import com.spotify.home.dac.component.experimental.v1.proto.FilterComponent
17 |
18 | @OptIn(ExperimentalMaterial3Api::class)
19 | @Composable
20 | fun FilterComponentBinder (
21 | component: FilterComponent,
22 | selectedFacet: String,
23 | selectFacet: (String) -> Unit,
24 | ) {
25 | LazyRow(Modifier.padding(start = 16.dp, bottom = 4.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
26 | items(component.facetsList) { item ->
27 | val selected = selectedFacet == item.value
28 | FilterChip(selected = selected, onClick = {
29 | selectFacet(if (selected) "default" else item.value)
30 | }, label = {
31 | Text(item.title)
32 | }, leadingIcon = {
33 | if (selected) Icon(Icons.Rounded.Check, null)
34 | })
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.foundation.shape.RoundedCornerShape
6 | import androidx.compose.material3.*
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.SideEffect
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.platform.LocalContext
11 | import androidx.compose.ui.unit.dp
12 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
13 |
14 | @Composable
15 | fun ApplicationTheme(
16 | darkTheme: Boolean = isSystemInDarkTheme(),
17 | content: @Composable () -> Unit
18 | ) {
19 | val sysUiController = rememberSystemUiController()
20 |
21 | SideEffect {
22 | sysUiController.setSystemBarsColor(color = Color.Transparent, darkIcons = !darkTheme)
23 | }
24 |
25 | MaterialTheme(
26 | colorScheme = provideColorScheme(darkTheme),
27 | shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(8.dp)),
28 | content = content
29 | )
30 | }
31 |
32 | @Composable
33 | private fun provideColorScheme(darkTheme: Boolean): ColorScheme {
34 | return when {
35 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> if (darkTheme) {
36 | dynamicDarkColorScheme(LocalContext.current)
37 | } else {
38 | dynamicLightColorScheme(LocalContext.current)
39 | }
40 | else -> if (darkTheme) {
41 | darkColorScheme()
42 | } else {
43 | lightColorScheme()
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/HomeSectionLargeHeader.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.foundation.shape.CircleShape
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.clip
12 | import androidx.compose.ui.unit.dp
13 | import androidx.compose.ui.unit.sp
14 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
15 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
16 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
17 | import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage
18 | import bruhcollective.itaysonlab.jetispot.ui.shared.SubtextOverline
19 |
20 | @Composable
21 | fun HomeSectionLargeHeader (
22 | item: HubItem
23 | ) {
24 | Row(Modifier.padding(vertical = 8.dp).clickableHub(item)) {
25 | PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier
26 | .size(48.dp)
27 | .clip(CircleShape))
28 |
29 | Column(Modifier.padding(horizontal = 12.dp).align(Alignment.CenterVertically)) {
30 | SubtextOverline(item.text!!.subtitle!!.uppercase(), modifier = Modifier)
31 | MediumText(item.text.title!!, modifier = Modifier.padding(top = 2.dp), fontSize = 21.sp)
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/SingleFocusCard.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.Card
5 | import androidx.compose.material3.CardDefaults
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.unit.dp
11 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
12 | import bruhcollective.itaysonlab.jetispot.ui.ext.compositeSurfaceElevation
13 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
14 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
15 | import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage
16 | import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext
17 |
18 | @Composable
19 | fun SingleFocusCard (
20 | item: HubItem
21 | ) {
22 | Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.compositeSurfaceElevation(3.dp)), modifier = Modifier
23 | .height(120.dp)
24 | .fillMaxWidth()
25 | .clickableHub(item)) {
26 | Row {
27 | PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier
28 | .fillMaxHeight()
29 | .width(120.dp))
30 | Box(Modifier.fillMaxSize().padding(16.dp)) {
31 | Column(Modifier.align(Alignment.TopStart)) {
32 | MediumText(text = item.text!!.title!!)
33 | Subtext(text = item.text.subtitle!!)
34 | }
35 | }
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.enableR8.fullMode=true
24 | android.nonTransitiveRClass=true
25 | android.enableResourceOptimizations=true
26 | android.enableAppCompileTimeRClass=true
27 | android.useMinimalKeepRules=true
28 | android.includeDependencyInfoInApks=false
29 | android.defaults.buildfeatures.buildconfig=true
30 | android.nonFinalResIds=false
--------------------------------------------------------------------------------
/app/src/main/proto/sp_ext/color_lyrics.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_multiple_files = true;
4 | option java_package = "com.spotify.lyrics.v2.lyrics.proto";
5 |
6 | // com.spotify.colorlyrics.proto
7 | message ColorLyricsResponse {
8 | message ColorData {
9 | int32 background = 1;
10 | int32 text = 2;
11 | int32 highlight_text = 3;
12 | }
13 |
14 | LyricsResponse lyrics = 1;
15 | optional ColorData colors = 2;
16 | bool has_vocal_removal = 3;
17 | optional ColorData vocal_removal_colors = 4;
18 | }
19 |
20 | message LyricsResponse {
21 | enum SyncType {
22 | UNSYNCED = 0;
23 | LINE_SYNCED = 1;
24 | SYLLABLE_SYNCED = 2;
25 | }
26 |
27 | message LyricsLine {
28 | int64 start_time_ms = 1;
29 | string words = 2;
30 | repeated Syllable syllables = 3;
31 |
32 | message Syllable {
33 | int64 start_time_ms = 1;
34 | int64 num_chars = 2;
35 | }
36 | }
37 |
38 | message AndroidIntent {
39 | string provider = 1;
40 | string provider_android_app_id = 2;
41 | string action = 3;
42 | string data = 4;
43 | string content_type = 5;
44 | }
45 |
46 | message Alternative {
47 | string language = 1;
48 | repeated LyricsLine lines = 2;
49 | bool is_rtl_language = 3;
50 | }
51 |
52 | SyncType syncType = 1;
53 | repeated LyricsLine lines = 2;
54 | string provider = 3;
55 | string provider_lyrics_id = 4;
56 | string provider_display_name = 5;
57 | optional AndroidIntent sync_lyrics_android_intent = 6;
58 | string sync_lyrics_uri = 7;
59 | bool is_dense_typeface = 8;
60 | repeated Alternative alternatives = 9;
61 | string language = 10;
62 | bool is_rtl_language = 11;
63 | int32 fullscreen_action = 12;
64 | }
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/Carousel.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.lazy.LazyRow
7 | import androidx.compose.foundation.lazy.items
8 | import androidx.compose.material.Text
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.text.font.FontWeight
13 | import androidx.compose.ui.unit.dp
14 | import androidx.compose.ui.unit.sp
15 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
16 | import bruhcollective.itaysonlab.jetispot.ui.hub.HubBinder
17 | import bruhcollective.itaysonlab.jetispot.ui.hub.LocalHubScreenDelegate
18 |
19 | @Composable
20 | fun Carousel(
21 | item: HubItem,
22 | ) {
23 | val isSurroundedWithPadding = LocalHubScreenDelegate.current.isSurroundedWithPadding()
24 |
25 | Column(Modifier.padding(vertical = if (isSurroundedWithPadding) 0.dp else 8.dp)) {
26 | if (item.text != null) {
27 | Text(text = item.text.title!!, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 16.sp,
28 | modifier = Modifier.padding(horizontal = if (isSurroundedWithPadding) 0.dp else 16.dp).padding(bottom = 12.dp))
29 | }
30 |
31 | LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.padding(horizontal = if (isSurroundedWithPadding) 0.dp else 16.dp)) {
32 | items(item.children ?: listOf()) { cItem ->
33 | HubBinder(cItem)
34 | }
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/navigation/NavigationController.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.navigation
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.annotation.StringRes
6 | import androidx.compose.runtime.Immutable
7 | import androidx.compose.runtime.staticCompositionLocalOf
8 | import androidx.navigation.NavHostController
9 | import bruhcollective.itaysonlab.jetispot.ui.screens.BottomSheet
10 | import bruhcollective.itaysonlab.jetispot.ui.screens.Dialog
11 | import bruhcollective.itaysonlab.jetispot.ui.screens.Screen
12 |
13 | @JvmInline
14 | @Immutable
15 | value class NavigationController(
16 | val controller: () -> NavHostController
17 | ) {
18 | // Not recommended
19 | fun navigate(route: String) = controller().navigate(route)
20 |
21 | fun navigate(screen: Screen) = controller().navigate(screen.route)
22 | fun navigate(dialog: Dialog) = controller().navigate(dialog.route)
23 |
24 | fun navigate(sheet: BottomSheet, args: Map) {
25 | var url = sheet.route
26 |
27 | args.forEach { entry ->
28 | url = url.replace("{${entry.key}}", entry.value)
29 | }
30 |
31 | controller().navigate(url)
32 | }
33 |
34 | fun navigateAndClearStack(screen: Screen) = controller().navigate(screen.route) { popUpTo(Screen.NavGraph.route) }
35 |
36 | fun popBackStack() = controller().popBackStack()
37 |
38 | fun context() = controller().context
39 | fun string(@StringRes id: Int) = context().getString(id)
40 | fun openInBrowser(uri: String) = context().startActivity(Intent(Intent.ACTION_VIEW).setData(Uri.parse(uri)))
41 | }
42 |
43 | val LocalNavigationController = staticCompositionLocalOf { error("Supply NavigationController in composable scope!") }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/BrowseScreen.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.screens.hub
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.rounded.ArrowBack
6 | import androidx.compose.material3.*
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.input.nestedscroll.nestedScroll
10 | import androidx.compose.ui.platform.LocalDensity
11 | import androidx.compose.ui.text.style.TextOverflow
12 | import androidx.compose.ui.unit.dp
13 | import bruhcollective.itaysonlab.jetispot.ui.ext.rememberEUCScrollBehavior
14 | import bruhcollective.itaysonlab.jetispot.ui.navigation.LocalNavigationController
15 |
16 | @OptIn(ExperimentalMaterial3Api::class)
17 | @Composable
18 | fun BrowseScreen(
19 | id: String
20 | ) {
21 | val navController = LocalNavigationController.current
22 | val scrollBehavior = rememberEUCScrollBehavior()
23 | var appBarTitle by remember { mutableStateOf("") }
24 |
25 | Scaffold(topBar = {
26 | LargeTopAppBar(title = {
27 | Text(appBarTitle, maxLines = 1, overflow = TextOverflow.Ellipsis)
28 | }, navigationIcon = {
29 | IconButton(onClick = { navController.popBackStack() }) {
30 | Icon(Icons.Rounded.ArrowBack, null)
31 | }
32 | }, colors = TopAppBarDefaults.largeTopAppBarColors(), scrollBehavior = scrollBehavior)
33 | }, modifier = Modifier.fillMaxSize().nestedScroll(scrollBehavior.nestedScrollConnection), contentWindowInsets = WindowInsets(top = 0.dp)) { padding ->
34 | Box(Modifier.padding(padding)) {
35 | HubScreen(
36 | needContentPadding = false,
37 | loader = { getBrowseView(id) },
38 | onAppBarTitleChange = { appBarTitle = it }
39 | )
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/NavController.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.shared
2 |
3 | import androidx.compose.foundation.LocalIndication
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.interaction.MutableInteractionSource
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.composed
9 | import bruhcollective.itaysonlab.jetispot.ui.hub.HubScreenDelegate
10 | import bruhcollective.itaysonlab.jetispot.ui.hub.LocalHubScreenDelegate
11 | import bruhcollective.itaysonlab.jetispot.ui.navigation.LocalNavigationController
12 | import bruhcollective.itaysonlab.jetispot.ui.navigation.NavigationController
13 |
14 | fun Modifier.navClickable(
15 | enabled: Boolean = true,
16 | enableRipple: Boolean = true,
17 | onClick: (NavigationController) -> Unit
18 | ) = composed {
19 | val navController = LocalNavigationController.current
20 |
21 | Modifier.clickable(
22 | enabled = enabled,
23 | indication = if (enableRipple) LocalIndication.current else null,
24 | interactionSource = remember { MutableInteractionSource() },
25 | ) {
26 | onClick(navController)
27 | }
28 | }
29 |
30 | fun Modifier.navAndHubClickable(
31 | enabled: Boolean = true,
32 | enableRipple: Boolean = true,
33 | onClick: (NavigationController, HubScreenDelegate) -> Unit
34 | ) = composed {
35 | val navController = LocalNavigationController.current
36 | val hubScreenDelegate = LocalHubScreenDelegate.current
37 |
38 | Modifier.clickable(
39 | enabled = enabled,
40 | indication = if (enableRipple) LocalIndication.current else null,
41 | interactionSource = remember { MutableInteractionSource() },
42 | ) {
43 | onClick(navController, hubScreenDelegate)
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/shared/SharedTexts.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.shared
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.material3.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.text.font.FontWeight
9 | import androidx.compose.ui.text.style.TextAlign
10 | import androidx.compose.ui.text.style.TextOverflow
11 | import androidx.compose.ui.unit.TextUnit
12 | import androidx.compose.ui.unit.sp
13 |
14 | @Composable
15 | fun MediumText (
16 | text: String,
17 | modifier: Modifier = Modifier,
18 | color: Color = Color.Unspecified,
19 | fontWeight: FontWeight = FontWeight.Bold,
20 | lineHeight: TextUnit = TextUnit.Unspecified,
21 | maxLines: Int = 1,
22 | fontSize: TextUnit = 16.sp,
23 | textAlign: TextAlign? = null,
24 | ) {
25 | Text(text, textAlign = textAlign, color = color, fontSize = fontSize, fontWeight = fontWeight, maxLines = maxLines, lineHeight = lineHeight, overflow = TextOverflow.Ellipsis, modifier = modifier)
26 | }
27 |
28 | @Composable
29 | fun Subtext (
30 | text: String,
31 | modifier: Modifier = Modifier,
32 | fontSize: TextUnit = 12.sp,
33 | maxLines: Int = 2,
34 | textAlign: TextAlign? = null,
35 | ) {
36 | Text(text, textAlign = textAlign, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), fontSize = fontSize, lineHeight = 18.sp, maxLines = maxLines, overflow = TextOverflow.Ellipsis, modifier = modifier)
37 | }
38 |
39 | @Composable
40 | fun SubtextOverline (
41 | text: String,
42 | modifier: Modifier = Modifier
43 | ) {
44 | Text(text, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), letterSpacing = 2.sp, fontSize = 12.sp, lineHeight = 18.sp, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = modifier)
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/MediumCard.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.foundation.layout.size
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.draw.clip
11 | import androidx.compose.ui.unit.dp
12 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
13 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
14 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
15 | import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage
16 | import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext
17 |
18 | @Composable
19 | fun MediumCard(
20 | item: HubItem
21 | ) {
22 | val size = 160.dp
23 |
24 | Column(Modifier.width(size).clickableHub(item)) {
25 | var drawnTitle = false
26 |
27 | PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier.size(size).clip(
28 | RoundedCornerShape(if (item.images?.main?.isRounded == true) 12.dp else 0.dp)
29 | ))
30 |
31 | if (!item.text?.title.isNullOrEmpty()) {
32 | drawnTitle = true
33 | MediumText(item.text!!.title!!, modifier = Modifier.padding(top = 8.dp))
34 | }
35 |
36 | if (!item.text?.subtitle.isNullOrEmpty()) {
37 | Subtext(item.text!!.subtitle!!, modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp))
38 | } else if (!item.text?.description.isNullOrEmpty()) {
39 | Subtext(item.text!!.description!!, modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp))
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/SpAuthManager.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core
2 |
3 | import bruhcollective.itaysonlab.jetispot.core.collection.SpCollectionManager
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.withContext
6 | import xyz.gianlu.librespot.core.Session
7 | import java.io.File
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | @Singleton
12 | class SpAuthManager @Inject constructor(
13 | private val spSessionManager: SpSessionManager,
14 | private val spPlayerManager: SpPlayerManager,
15 | private val spCollectionManager: SpCollectionManager
16 | ) {
17 | @Suppress("BlockingMethodInNonBlockingContext")
18 | suspend fun authWith(username: String, password: String) = withContext(Dispatchers.IO) {
19 | try {
20 | spSessionManager.setSession(spSessionManager.createSession().userPass(username, password).create())
21 | spPlayerManager.createPlayer()
22 | spCollectionManager.init()
23 | AuthResult.Success
24 | } catch (se: Session.SpotifyAuthenticationException) {
25 | AuthResult.SpError(se.message ?: "Unknown error")
26 | } catch (e: Exception) {
27 | e.printStackTrace()
28 | AuthResult.Exception(e)
29 | }
30 | }
31 |
32 | @Suppress("BlockingMethodInNonBlockingContext")
33 | suspend fun authStored() = withContext(Dispatchers.IO) {
34 | runCatching {
35 | spSessionManager.setSession(spSessionManager.createSession().stored().create())
36 | spPlayerManager.createPlayer()
37 | spCollectionManager.init()
38 | }
39 | }
40 |
41 | sealed class AuthResult {
42 | object Success: AuthResult()
43 | class SpError(val msg: String): AuthResult()
44 | class Exception(val e: kotlin.Exception): AuthResult()
45 | }
46 |
47 | fun reset() {
48 | File(spSessionManager.appContext.filesDir, "spa_creds").delete()
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/BrowseRadioScreen.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.screens.hub
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.WindowInsets
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.rounded.ArrowBack
9 | import androidx.compose.material3.*
10 | import androidx.compose.runtime.*
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.input.nestedscroll.nestedScroll
13 | import androidx.compose.ui.text.style.TextOverflow
14 | import androidx.compose.ui.unit.dp
15 | import bruhcollective.itaysonlab.jetispot.ui.ext.rememberEUCScrollBehavior
16 | import bruhcollective.itaysonlab.jetispot.ui.navigation.LocalNavigationController
17 |
18 | @OptIn(ExperimentalMaterial3Api::class)
19 | @Composable
20 | fun BrowseRadioScreen() {
21 | val navController = LocalNavigationController.current
22 | val scrollBehavior = rememberEUCScrollBehavior()
23 | var appBarTitle by remember { mutableStateOf("") }
24 |
25 | Scaffold(topBar = {
26 | LargeTopAppBar(title = {
27 | Text(appBarTitle, maxLines = 1, overflow = TextOverflow.Ellipsis)
28 | }, navigationIcon = {
29 | IconButton(onClick = { navController.popBackStack() }) {
30 | Icon(Icons.Rounded.ArrowBack, null)
31 | }
32 | }, colors = TopAppBarDefaults.largeTopAppBarColors(), scrollBehavior = scrollBehavior)
33 | }, modifier = Modifier.fillMaxSize().nestedScroll(scrollBehavior.nestedScrollConnection), contentWindowInsets = WindowInsets(top = 0.dp)) { padding ->
34 | Box(Modifier.padding(padding)) {
35 | HubScreen(
36 | needContentPadding = false,
37 | loader = { getRadioHub() },
38 | onAppBarTitleChange = { appBarTitle = it }
39 | )
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/search/SearchViewModel.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.screens.search
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.compose.ui.text.input.TextFieldValue
7 | import androidx.lifecycle.ViewModel
8 | import bruhcollective.itaysonlab.jetispot.core.SpPlayerServiceManager
9 | import bruhcollective.itaysonlab.jetispot.core.api.SpInternalApi
10 | import bruhcollective.itaysonlab.jetispot.core.objs.player.PfcContextData
11 | import bruhcollective.itaysonlab.jetispot.core.objs.player.PlayFromContextPlayerData
12 | import bruhcollective.itaysonlab.jetispot.core.util.playCommand
13 | import bruhcollective.itaysonlab.jetispot.proto.SearchViewResponse
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.CoroutineScope
16 | import kotlinx.coroutines.MainScope
17 | import javax.inject.Inject
18 |
19 | @HiltViewModel
20 | class SearchViewModel @Inject constructor(
21 | private val spInternalApi: SpInternalApi,
22 | private val spPlayerServiceManager: SpPlayerServiceManager
23 | ): ViewModel(), CoroutineScope by MainScope() {
24 | var searchQuery by mutableStateOf(TextFieldValue())
25 |
26 | var searchResponse by mutableStateOf(null)
27 | private set
28 |
29 | suspend fun initiateSearch() {
30 | searchResponse = null
31 | searchResponse = spInternalApi.search(
32 | query = searchQuery.text
33 | )
34 | }
35 |
36 | fun dispatchPlay(uri: String) {
37 | spPlayerServiceManager.play(
38 | playCommand(uri) {
39 | contextUri = "spotify:search:${searchQuery.text.replace(" ", "+")}"
40 | }
41 | )
42 | }
43 |
44 | fun clear() {
45 | searchQuery = TextFieldValue()
46 | searchResponse = null
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp_ext/contentfeed.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/timestamp.proto";
4 |
5 | package com.spotify.contentfeed.proto.v1.common;
6 |
7 | option java_multiple_files = true;
8 | option java_package = "com.spotify.contentfeed.proto.v1.common";
9 |
10 | message FeedItem {
11 | string id = 1;
12 | ContentType contentType = 2;
13 | string deduplicationKey = 3;
14 | string targetUri = 4;
15 | google.protobuf.Timestamp timestamp = 5;
16 | FeedItemState state = 6;
17 |
18 | oneof typeSpecificAttributes {
19 | MusicRelease musicRelease = 7;
20 | PodcastEpisodeRelease podcastEpisodeRelease = 8;
21 | }
22 | }
23 |
24 | message FeedItemState {
25 | enum InteractionState {
26 | INTERACTION_STATE_INVALID = 0;
27 | NEW = 1;
28 | SEEN = 2;
29 | }
30 |
31 | string itemId = 1;
32 | InteractionState interactionState = 2;
33 | optional google.protobuf.Timestamp timestamp = 3;
34 | }
35 |
36 | enum ContentType {
37 | CONTENT_TYPE_INVALID = 0;
38 | MUSIC_RELEASE = 1;
39 | PODCAST_EPISODE_RELEASE = 2;
40 | RECOMMENDATION = 3;
41 | ARTIST_OFFER = 5;
42 | }
43 |
44 | message MusicRelease {
45 | AlbumType albumType = 1;
46 | repeated string artists = 2;
47 | optional bool explicit = 3;
48 | string albumName = 4;
49 | string imageUrl = 5;
50 | }
51 |
52 | message PodcastEpisodeRelease {
53 | string showName = 1;
54 | int32 episodeType = 2; // enum?
55 | int32 durationMilliseconds = 3;
56 | bool explicit = 4;
57 | string episodeName = 5;
58 | string imageUrl = 6;
59 | bool is19PlusOnly = 7;
60 | bool isMusicAndTalk = 8;
61 | bool isPaywalled = 9;
62 | bool isUserSubscribed = 10;
63 | repeated string musicAndTalkArtistNames = 11;
64 | optional string episodeDescription = 12;
65 | }
66 |
67 | enum AlbumType {
68 | UNSPECIFIED = 0;
69 | ALBUM = 1;
70 | SINGLE = 2;
71 | COMPILATION = 3;
72 | EP = 4;
73 | AUDIOBOOK = 5;
74 | PODCAST = 6;
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/blend/BlendCreateInvitationScreen.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.screens.blend
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.hilt.navigation.compose.hiltViewModel
5 | import androidx.lifecycle.ViewModel
6 | import bruhcollective.itaysonlab.jetispot.R
7 | import bruhcollective.itaysonlab.jetispot.core.SpConfigurationManager
8 | import bruhcollective.itaysonlab.jetispot.core.api.SpBlendApi
9 | import bruhcollective.itaysonlab.jetispot.proto.AppConfig
10 | import bruhcollective.itaysonlab.jetispot.ui.ext.shareUrl
11 | import bruhcollective.itaysonlab.jetispot.ui.screens.config.BaseConfigScreen
12 | import bruhcollective.itaysonlab.jetispot.ui.screens.config.ConfigItem
13 | import bruhcollective.itaysonlab.jetispot.ui.screens.config.ConfigViewModel
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.*
16 | import javax.inject.Inject
17 |
18 | @Composable
19 | fun BlendCreateInvitationScreen (
20 | viewModel: BlendCreateInvitationViewModel = hiltViewModel()
21 | ) {
22 | BaseConfigScreen(viewModel)
23 | }
24 |
25 | @HiltViewModel
26 | class BlendCreateInvitationViewModel @Inject constructor(
27 | private val spBlendApi: SpBlendApi
28 | ) : ViewModel(), ConfigViewModel, CoroutineScope by MainScope() {
29 | override suspend fun modifyDatastore(runOnBuilder: AppConfig.Builder.() -> Unit) {}
30 | override fun provideDataStore() = SpConfigurationManager.EMPTY
31 | override fun provideTitle() = R.string.blend_create
32 |
33 | override fun provideConfigList() = buildList {
34 | add(ConfigItem.Preference(
35 | R.string.blend_create_btn, { _, _ -> "" }
36 | ) {
37 | launch {
38 | val link = withContext(Dispatchers.IO) { spBlendApi.generateBlend().invite }
39 | it.context().shareUrl(link, it.string(R.string.blend_invite))
40 | }
41 | })
42 |
43 | add(ConfigItem.Info(R.string.blend_info))
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ArtistTrackRow.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.text.font.FontWeight
12 | import androidx.compose.ui.unit.dp
13 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
14 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
15 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
16 | import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage
17 | import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext
18 |
19 | @Composable
20 | fun ArtistTrackRow(
21 | item: HubItem
22 | ) {
23 | Row(
24 | Modifier
25 | .clickableHub(item)
26 | .padding(horizontal = 16.dp, vertical = 12.dp)) {
27 | Text(text = (item.custom!!["rowNumber"] as Double).toInt().toString(), modifier = Modifier
28 | .align(Alignment.CenterVertically)
29 | .padding(end = 16.dp))
30 |
31 | PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier.align(Alignment.CenterVertically).size(48.dp))
32 |
33 | Column(Modifier.align(Alignment.CenterVertically).padding(start = 16.dp)) {
34 | var drawnTitle = false
35 |
36 | if (!item.text?.title.isNullOrEmpty()) {
37 | drawnTitle = true
38 | MediumText(item.text!!.title!!, fontWeight = FontWeight.Normal)
39 | }
40 |
41 | if (!item.text?.subtitle.isNullOrEmpty()) {
42 | Subtext(item.text!!.subtitle!!, modifier = Modifier.padding(top = if (drawnTitle) 4.dp else 8.dp))
43 | }
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_plans/PlanComponentBinder.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.dac.components_plans
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.shape.RoundedCornerShape
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.rounded.Paid
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.Surface
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.draw.clip
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.unit.dp
15 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
16 | import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext
17 | import bruhcollective.itaysonlab.jetispot.ui.shared.navClickable
18 | import com.spotify.allplans.v1.PlanComponent
19 |
20 | @Composable
21 | fun PlanComponentBinder(
22 | item: PlanComponent
23 | ) {
24 | Row(Modifier.navClickable { navController ->
25 | navController.openInBrowser(item.uri)
26 | }.fillMaxWidth().padding(horizontal = 16.dp)) {
27 | Surface(
28 | tonalElevation = 32.dp, modifier = Modifier
29 | .clip(RoundedCornerShape(8.dp))
30 | .size(56.dp)
31 | ) {
32 | Icon(
33 | Icons.Rounded.Paid,
34 | null,
35 | tint = Color(android.graphics.Color.parseColor(item.planColor)),
36 | modifier = Modifier
37 | .padding(12.dp)
38 | .fillMaxSize()
39 | )
40 | }
41 |
42 | Column(
43 | Modifier
44 | .padding(start = 16.dp)
45 | .align(Alignment.CenterVertically)
46 | ) {
47 | MediumText(text = item.planName)
48 | Subtext(text = item.planPrice, modifier = Modifier.padding(top = 4.dp))
49 | Subtext(text = item.availableAccounts, modifier = Modifier.padding(top = 4.dp))
50 | }
51 | }
52 |
53 | Spacer(modifier = Modifier.height(8.dp))
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/Screen.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.screens
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.rounded.LibraryMusic
6 | import androidx.compose.material.icons.rounded.Search
7 | import androidx.compose.material.icons.rounded.Home
8 | import androidx.compose.runtime.Immutable
9 | import bruhcollective.itaysonlab.jetispot.R
10 |
11 | @Immutable
12 | enum class Screen(
13 | val route: String,
14 | @StringRes val title: Int = 0,
15 | ) {
16 | // internal
17 | NavGraph("nav_graph"),
18 | CoreLoading("coreLoading"),
19 | Authorization("auth"),
20 | SpotifyIdRedirect("spotify:{uri}"),
21 | // bottom
22 | Feed("feed", title = R.string.tab_home),
23 | Search("search", title = R.string.tab_search),
24 | Library("library", title = R.string.tab_library),
25 | // hubs/dac
26 | DacViewCurrentPlan("dac/viewCurrentPlan", title = R.string.plan_overview),
27 | DacViewAllPlans("dac/viewAllPlans", title = R.string.all_plans),
28 | // config
29 | Config("config"),
30 | StorageConfig("config/storage"),
31 | QualityConfig("config/playbackQuality"),
32 | NormalizationConfig("config/playbackNormalization");
33 |
34 | companion object {
35 | val hideNavigationBar = setOf(CoreLoading.route, Authorization.route, Dialog.AuthDisclaimer.route)
36 | val deeplinkCapable = mapOf(SpotifyIdRedirect to "https://open.spotify.com/{type}/{typeId}")
37 | val showInBottomNavigation = mapOf(
38 | Feed to Icons.Rounded.Home,
39 | Search to Icons.Rounded.Search,
40 | Library to Icons.Rounded.LibraryMusic
41 | )
42 | }
43 | }
44 |
45 | @Immutable
46 | enum class Dialog(
47 | val route: String
48 | ) {
49 | AuthDisclaimer("dialogs/disclaimers"),
50 | Logout("dialogs/logout")
51 | }
52 |
53 | @Immutable
54 | enum class BottomSheet(
55 | val route: String
56 | ) {
57 | JumpToArtist("bs/jumpToArtist/{artistIdsAndRoles}") // ID=ROLE|ID=ROLE
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/PodcastShowScreen.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.screens.hub
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.hilt.navigation.compose.hiltViewModel
7 | import bruhcollective.itaysonlab.jetispot.core.SpMetadataRequester
8 | import bruhcollective.itaysonlab.jetispot.core.SpPlayerServiceManager
9 | import bruhcollective.itaysonlab.jetispot.core.SpSessionManager
10 | import bruhcollective.itaysonlab.jetispot.core.api.SpPartnersApi
11 | import bruhcollective.itaysonlab.jetispot.core.objs.player.PlayFromContextData
12 | import bruhcollective.itaysonlab.jetispot.ui.hub.virt.ShowEntityView
13 | import dagger.hilt.android.lifecycle.HiltViewModel
14 | import javax.inject.Inject
15 |
16 | @Composable
17 | fun PodcastShowScreen(
18 | id: String,
19 | viewModel: PodcastShowViewModel = hiltViewModel()
20 | ) {
21 | LaunchedEffect(Unit) {
22 | viewModel.load { viewModel.loadInternal(id) }
23 | }
24 |
25 | HubScaffold(
26 | appBarTitle = viewModel.title.value,
27 | state = viewModel.state,
28 | viewModel = viewModel
29 | ) {
30 | viewModel.reload { viewModel.loadInternal(id) }
31 | }
32 | }
33 |
34 | @HiltViewModel
35 | class PodcastShowViewModel @Inject constructor(
36 | private val spSessionManager: SpSessionManager,
37 | private val spPartnersApi: SpPartnersApi,
38 | private val spPlayerServiceManager: SpPlayerServiceManager,
39 | private val spMetadataRequester: SpMetadataRequester
40 | ) : AbsHubViewModel() {
41 | val title = mutableStateOf("")
42 |
43 | suspend fun loadInternal(id: String) = ShowEntityView.create(spSessionManager, spMetadataRequester, id).also { title.value = it.title ?: "" }
44 |
45 | override fun play(data: PlayFromContextData) = play(spPlayerServiceManager, data)
46 | override suspend fun calculateDominantColor(url: String, dark: Boolean) = calculateDominantColor(spPartnersApi, url, dark)
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/nowplaying/NowPlayingScreen.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.material.BottomSheetState
9 | import androidx.compose.material.ExperimentalMaterialApi
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.rememberCoroutineScope
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.draw.alpha
15 | import androidx.compose.ui.unit.dp
16 | import androidx.hilt.navigation.compose.hiltViewModel
17 | import bruhcollective.itaysonlab.jetispot.ui.screens.nowplaying.fullscreen.NowPlayingFullscreenComposition
18 | import kotlinx.coroutines.launch
19 |
20 | @Composable
21 | @OptIn(ExperimentalMaterialApi::class)
22 | fun NowPlayingScreen(
23 | bottomSheetState: BottomSheetState,
24 | bsOffset: () -> Float,
25 | queueOpened: Boolean,
26 | setQueueOpened: (Boolean) -> Unit,
27 | lyricsOpened: Boolean,
28 | setLyricsOpened: (Boolean) -> Unit,
29 | viewModel: NowPlayingViewModel = hiltViewModel()
30 | ) {
31 | val scope = rememberCoroutineScope()
32 |
33 | Box(Modifier.fillMaxSize()) {
34 | NowPlayingFullscreenComposition(
35 | queueOpened = queueOpened,
36 | setQueueOpened = setQueueOpened,
37 | lyricsOpened = lyricsOpened,
38 | setLyricsOpened = setLyricsOpened,
39 | bottomSheetState = bottomSheetState,
40 | viewModel = viewModel
41 | )
42 |
43 | NowPlayingMiniplayer(
44 | viewModel,
45 | Modifier
46 | .alpha(1f - bsOffset())
47 | .clickable { scope.launch { bottomSheetState.expand() } }
48 | .fillMaxWidth()
49 | .height(72.dp)
50 | .align(Alignment.TopStart)
51 | )
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/auth/AuthScreenViewModel.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.screens.auth
2 |
3 | import android.content.res.Resources
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.State
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import bruhcollective.itaysonlab.jetispot.R
10 | import bruhcollective.itaysonlab.jetispot.core.SpAuthManager
11 | import dagger.hilt.android.lifecycle.HiltViewModel
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 |
15 | @Stable
16 | @HiltViewModel
17 | class AuthScreenViewModel @Inject constructor(
18 | private val authManager: SpAuthManager,
19 | private val resources: Resources,
20 | ) : ViewModel() {
21 |
22 | private val _isAuthInProgress = mutableStateOf(false)
23 | val isAuthInProgress: State = _isAuthInProgress
24 |
25 | fun auth(
26 | username: String,
27 | password: String,
28 | onSuccess: () -> Unit,
29 | onFailure: (String) -> Unit,
30 | ) {
31 | if (isAuthInProgress.value) return
32 |
33 | viewModelScope.launch {
34 | if (username.isEmpty() || password.isEmpty()) {
35 | onFailure(resources.getString(R.string.auth_err_empty))
36 | return@launch
37 | }
38 |
39 | _isAuthInProgress.value = true
40 |
41 | when (val result = authManager.authWith(username, password)) {
42 | SpAuthManager.AuthResult.Success -> onSuccess()
43 | is SpAuthManager.AuthResult.Exception -> onFailure("Java Error: ${result.e.message}")
44 | is SpAuthManager.AuthResult.SpError -> onFailure(
45 | when (result.msg) {
46 | "BadCredentials" -> resources.getString(R.string.auth_err_badcreds)
47 | "PremiumAccountRequired" -> resources.getString(R.string.auth_err_premium)
48 | else -> "Spotify API error: ${result.msg}"
49 | }
50 | )
51 | }
52 |
53 | _isAuthInProgress.value = false
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/ArtistHeader.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.Brush
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.TileMode
11 | import androidx.compose.ui.layout.ContentScale
12 | import androidx.compose.ui.platform.LocalConfiguration
13 | import androidx.compose.ui.unit.dp
14 | import androidx.compose.ui.unit.sp
15 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
16 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
17 | import coil.compose.AsyncImage
18 |
19 | @Composable
20 | fun ArtistHeader(
21 | item: HubItem
22 | ) {
23 | Column(
24 | modifier = Modifier
25 | .size((LocalConfiguration.current.screenWidthDp).dp, 310.dp),
26 | ) {
27 | Box {
28 | AsyncImage(
29 | model = item.images?.main?.uri, contentDescription = null,
30 | Modifier.fillMaxWidth(),
31 | contentScale = ContentScale.FillWidth
32 | )
33 |
34 | Column(
35 | Modifier
36 | .background(
37 | Brush.verticalGradient(
38 | 0F to Color.Transparent,
39 | 1F to Color.Black,
40 | ),
41 | )
42 | .fillMaxSize()
43 | ) {}
44 |
45 | MediumText(
46 | text = item.text?.title!!,
47 | fontSize = 50.sp,
48 | color = Color.White,
49 | modifier = Modifier
50 | .align(Alignment.BottomStart)
51 | .padding(horizontal = 16.dp)
52 | .padding(bottom = 8.dp)
53 | )
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/hub/HubItem.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.objs.hub
2 |
3 | import com.squareup.moshi.JsonClass
4 | import java.util.*
5 |
6 | @JsonClass(generateAdapter = true)
7 | data class HubItem (
8 | val component: HubComponent,
9 | val id: String = UUID.randomUUID().toString(),
10 | // types
11 | val text: HubText? = null,
12 | val children: List? = null,
13 | val images: HubImages? = null,
14 | val events: HubEvents? = null,
15 | //
16 | val custom: Map? = null,
17 | val metadata: HubItemMetadata? = null
18 | )
19 |
20 | //
21 |
22 | @JsonClass(generateAdapter = true)
23 | class HubText (
24 | val title: String? = null,
25 | val subtitle: String? = null,
26 | val description: String? = null
27 | )
28 |
29 | @JsonClass(generateAdapter = true)
30 | class HubImages (
31 | val main: HubImage? = null,
32 | val background: HubImage? = null,
33 | )
34 |
35 | @JsonClass(generateAdapter = true)
36 | class HubImage (
37 | val uri: String? = null,
38 | val placeholder: String? = null,
39 | val custom: HubImageCustom? = null
40 | ) {
41 | val isRounded = custom?.style == "rounded"
42 | }
43 |
44 | @JsonClass(generateAdapter = true)
45 | class HubImageCustom (
46 | val style: String? = null
47 | )
48 |
49 | @JsonClass(generateAdapter = true)
50 | class HubEvents (
51 | val click: HubEvent?
52 | )
53 |
54 | @JsonClass(generateAdapter = true)
55 | class NavigateUri (
56 | val uri: String
57 | )
58 |
59 | @JsonClass(generateAdapter = true)
60 | class HubItemMetadata (
61 | val uri: String? = null,
62 | val album: JsonAlbum? = null,
63 | val artist: JsonArtist? = null,
64 | )
65 |
66 | @JsonClass(generateAdapter = true)
67 | class JsonAlbum (
68 | val year: Int,
69 | val artists: List,
70 | val type: String
71 | )
72 |
73 | @JsonClass(generateAdapter = true)
74 | class JsonArtist (
75 | val name: String?,
76 | val uri: String?,
77 | val images: List?
78 | )
79 |
80 | @JsonClass(generateAdapter = true)
81 | class JsonImage (
82 | val uri: String,
83 | val width: Int,
84 | val height: Int
85 | )
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/util/PlayCommandFactory.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.util
2 |
3 | import bruhcollective.itaysonlab.jetispot.core.objs.player.*
4 | import com.spotify.dac.player.v1.proto.PlayCommand
5 | import com.squareup.moshi.Moshi
6 | import com.squareup.moshi.adapter
7 |
8 | @DslMarker
9 | @Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
10 | annotation class PlayCommandDsl
11 |
12 | inline fun playCommand(uri: String, init: (@PlayCommandDsl PlayCommandBuilder).() -> Unit): PlayFromContextData {
13 | return PlayCommandBuilder(uri).apply(init).build()
14 | }
15 |
16 | class PlayCommandBuilder (
17 | private val contentUri: String
18 | ) {
19 | var contextUri: String = ""
20 | var skipToUri: String? = null
21 | var shuffle: Boolean = false
22 |
23 | fun build(): PlayFromContextData {
24 | require(contextUri.isNotEmpty()) { "Context URI should not be empty!" }
25 | require(contextUri.startsWith("spotify:")) { "Context URI should be a Spotify URI!" }
26 |
27 | skipToUri?.let {
28 | require(it.startsWith("spotify:track")) { "Context Skip-To URI should be a Spotify Track URI!" }
29 | }
30 |
31 | return PlayFromContextData(
32 | uri = contentUri,
33 | player = PlayFromContextPlayerData(
34 | context = PfcContextData(url = "context://$contextUri", uri = contextUri),
35 | options = PfcOptions(skip_to = skipToUri?.let { PfcOptSkipTo(track_uri = it) }, player_options_override = PfcStateOptions(shuffling_context = shuffle))
36 | )
37 | )
38 | }
39 | }
40 |
41 | fun PlayCommand.toApplicationPlayCommand(moshi: Moshi): PlayFromContextData {
42 | val context = moshi.adapter(PfcContextData::class.java).fromJson(this.context.toStringUtf8())!!
43 | val options = moshi.adapter(PlayFromContextPlayerData::class.java).fromJson(this.options.toStringUtf8())!!.options!!
44 |
45 | return PlayFromContextData(
46 | uri = context.uri,
47 | player = PlayFromContextPlayerData(
48 | context = context,
49 | options = options
50 | )
51 | )
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/playback/sp/LowToHighQualityPicker.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.playback.sp
2 |
3 | import bruhcollective.itaysonlab.jetispot.core.util.Log
4 | import com.spotify.metadata.Metadata
5 | import xyz.gianlu.librespot.audio.decoders.AudioQuality
6 | import xyz.gianlu.librespot.audio.decoders.VorbisOnlyAudioQuality
7 | import xyz.gianlu.librespot.audio.format.AudioQualityPicker
8 | import xyz.gianlu.librespot.audio.format.SuperAudioFormat
9 |
10 | /**
11 | * key features:
12 | * - support not only vorbis (Android also plays MP3)
13 | * - adaptive quality: don't use the first suitable file if quality is not found
14 | */
15 | class LowToHighQualityPicker(
16 | private val preferredQualityProvider: () -> AudioQuality
17 | ): AudioQualityPicker {
18 | companion object {
19 | private val ORDER = arrayOf(AudioQuality.LOW, AudioQuality.NORMAL, AudioQuality.HIGH, AudioQuality.VERY_HIGH, AudioQuality.FLAC)
20 | private val SUPPORTED_FORMATS = listOf(SuperAudioFormat.VORBIS, SuperAudioFormat.MP3)
21 | }
22 |
23 | private fun List.filterSupported() = filter { it.hasFileId() && it.hasFormat() }.filter {
24 | SUPPORTED_FORMATS.contains(SuperAudioFormat.get(it.format))
25 | }
26 |
27 | override fun getFile(files: MutableList): Metadata.AudioFile? {
28 | Log.d("LtHQP", "available quality: ${files.joinToString { it.format.name }}, preferred: ${preferredQualityProvider().name}")
29 | val preferredFiles = preferredQualityProvider().getMatches(files).filterSupported()
30 | if (preferredFiles.isEmpty()) {
31 | Log.d("LtHQP", "=> not found, searching for other")
32 | ORDER.forEach { aq ->
33 | val ordered = aq.getMatches(files).filterSupported()
34 | if (ordered.isNotEmpty()) {
35 | Log.d("LtHQP", "=> [${aq.name}] found: ${ordered.joinToString { it.format.name }}")
36 | return ordered[0]
37 | }
38 | }
39 | Log.d("LtHQP", "=> still not found")
40 | return null
41 | } else {
42 | Log.d("LtHQP", "=> found: ${preferredFiles.joinToString { it.format.name }}")
43 | return preferredFiles[0]
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/history/ListeningHistoryScreen.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.screens.history
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.rememberCoroutineScope
6 | import androidx.compose.ui.res.stringResource
7 | import androidx.hilt.navigation.compose.hiltViewModel
8 | import bruhcollective.itaysonlab.jetispot.R
9 | import bruhcollective.itaysonlab.jetispot.core.SpPlayerServiceManager
10 | import bruhcollective.itaysonlab.jetispot.core.api.SpInternalApi
11 | import bruhcollective.itaysonlab.jetispot.core.objs.player.PlayFromContextData
12 | import bruhcollective.itaysonlab.jetispot.ui.hub.HubScreenDelegate
13 | import bruhcollective.itaysonlab.jetispot.ui.screens.hub.AbsHubViewModel
14 | import bruhcollective.itaysonlab.jetispot.ui.screens.hub.HubScaffold
15 | import bruhcollective.itaysonlab.jetispot.ui.screens.hub.ToolbarOptions
16 | import dagger.hilt.android.lifecycle.HiltViewModel
17 | import kotlinx.coroutines.launch
18 | import javax.inject.Inject
19 |
20 | @Composable
21 | fun ListeningHistoryScreen(
22 | viewModel: HistoryViewModel = hiltViewModel()
23 | ) {
24 | val scope = rememberCoroutineScope()
25 |
26 | LaunchedEffect(Unit) {
27 | viewModel.load()
28 | }
29 |
30 | HubScaffold(
31 | appBarTitle = stringResource(id = R.string.listening_history),
32 | state = viewModel.state,
33 | viewModel = viewModel,
34 | reloadFunc = { scope.launch { viewModel.reload() } },
35 | toolbarOptions = ToolbarOptions(big = true, alwaysVisible = true)
36 | )
37 | }
38 |
39 | @HiltViewModel
40 | class HistoryViewModel @Inject constructor(
41 | private val spInternalApi: SpInternalApi,
42 | private val spPlayerServiceManager: SpPlayerServiceManager
43 | ) : AbsHubViewModel(), HubScreenDelegate {
44 | suspend fun load() = load {
45 | spInternalApi.getListeningHistory()
46 | }
47 |
48 | suspend fun reload() = reload {
49 | spInternalApi.getListeningHistory()
50 | }
51 |
52 | override fun play(data: PlayFromContextData) {
53 | spPlayerServiceManager.play(data.uri, data.player)
54 | }
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/core/objs/external/ExternalPersonalizedRecs.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.core.objs.external
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | @JsonClass(generateAdapter = true)
7 | class PersonalizedRecommendationsRequest(
8 | @Json(name = "external_accessory_description") val accessory: PersonalizedRecommendationsAccessory,
9 | @Json(name = "contextual_signals") val signals: List,
10 | @Json(name = "client_date_time") val dateTime: String, // ISO time
11 | )
12 |
13 | @JsonClass(generateAdapter = true)
14 | class PersonalizedRecommendationsAccessory(
15 | val integration: String,
16 | @Json(name = "client_id") val clientId: String,
17 | val name: String,
18 | @Json(name = "transport_type") val transportType: String,
19 | val category: String,
20 | val company: String,
21 | val model: String,
22 | val version: String,
23 | val protocol: String,
24 | @Json(name = "sender_id") val senderId: String,
25 | ) {
26 | companion object {
27 | val Auto = PersonalizedRecommendationsAccessory(
28 | integration = "android_auto",
29 | transportType = "bluetooth_or_usb",
30 | category = "car",
31 | protocol = "media_session",
32 | senderId = "com.google.android.projection.gearhead",
33 | clientId = "",
34 | name = "",
35 | company = "",
36 | model = "",
37 | version = ""
38 | )
39 | }
40 | }
41 |
42 | @JsonClass(generateAdapter = true)
43 | class PersonalizedRecommendationsResponse(
44 | @Json(name = "section_content") val content: List
45 | )
46 |
47 | @JsonClass(generateAdapter = true)
48 | class PersonalizedRecommendationsSection(
49 | val name: String,
50 | val title: String,
51 | val uri: String,
52 | @Json(name = "section_items") val items: List
53 | )
54 |
55 | @JsonClass(generateAdapter = true)
56 | class PersonalizedRecommendationsItem(
57 | val title: String,
58 | val subtitle: String?,
59 | val uri: String,
60 | @Json(name = "image_url") val image: String?
61 | )
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/playback/service/AudioFocusManager.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.playback.service
2 |
3 | import android.content.Context
4 | import android.media.AudioManager
5 | import androidx.core.content.getSystemService
6 | import androidx.media.AudioAttributesCompat
7 | import androidx.media.AudioFocusRequestCompat
8 | import androidx.media.AudioManagerCompat
9 | import bruhcollective.itaysonlab.jetispot.core.SpPlayerManager
10 | import bruhcollective.itaysonlab.jetispot.core.SpSessionManager
11 | import dagger.hilt.android.qualifiers.ApplicationContext
12 | import javax.inject.Inject
13 | import javax.inject.Singleton
14 |
15 | @Singleton
16 | class AudioFocusManager @Inject constructor(
17 | @ApplicationContext private val appContext: Context,
18 | private val spPlayerManager: SpPlayerManager,
19 | ) : AudioManager.OnAudioFocusChangeListener {
20 | private val audioManager = appContext.getSystemService()!!
21 |
22 | private val audioAttributes = AudioAttributesCompat.Builder().apply {
23 | setUsage(AudioAttributesCompat.USAGE_MEDIA)
24 | setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
25 | }.build()
26 |
27 | private val focusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN).apply {
28 | setAudioAttributes(audioAttributes)
29 | setOnAudioFocusChangeListener(this@AudioFocusManager)
30 | }.build()
31 |
32 | fun requestFocus() {
33 | AudioManagerCompat.requestAudioFocus(audioManager, focusRequest)
34 | }
35 |
36 | fun abandonFocus() {
37 | AudioManagerCompat.abandonAudioFocusRequest(audioManager, focusRequest)
38 | }
39 |
40 | override fun onAudioFocusChange(focusChange: Int) {
41 | when (focusChange) {
42 | AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
43 | spPlayerManager.playerNullable()?.pause()
44 | }
45 | AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
46 | spPlayerManager.playerNullable()?.volumeDown(24) // 64 total
47 | }
48 | AudioManager.AUDIOFOCUS_GAIN -> {
49 | spPlayerManager.playerNullable()?.play()
50 | spPlayerManager.playerNullable()?.volumeUp(24) // 64 total
51 | }
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/GridMediumCard.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.clip
12 | import androidx.compose.ui.text.style.TextAlign
13 | import androidx.compose.ui.unit.dp
14 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
15 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
16 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
17 | import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage
18 | import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext
19 |
20 | @Composable
21 | fun GridMediumCard(
22 | item: HubItem
23 | ) {
24 | val size = 160.dp
25 |
26 | Column(Modifier.fillMaxWidth().clickableHub(item)) {
27 | var drawnTitle = false
28 |
29 | PreviewableAsyncImage(imageUrl = item.images?.main?.uri, placeholderType = item.images?.main?.placeholder, modifier = Modifier.size(size).clip(
30 | RoundedCornerShape(if (item.images?.main?.isRounded == true) 12.dp else 0.dp)
31 | ).align(Alignment.CenterHorizontally))
32 |
33 | if (!item.text?.title.isNullOrEmpty()) {
34 | drawnTitle = true
35 | MediumText(item.text!!.title!!, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth().padding(top = 8.dp).padding(horizontal = 16.dp))
36 | }
37 |
38 | if (!item.text?.subtitle.isNullOrEmpty()) {
39 | 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))
40 | } else if (!item.text?.description.isNullOrEmpty()) {
41 | 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))
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Jetispot
2 | _probably usable __UNOFFICIAL__ Spotify client for Android, built with Jetpack Compose and librespot-java_
3 |
4 | #### 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.
5 |
6 | __What's working:__
7 | - sign in (login/pass only, no FB/Meta/whatsoever support, no Smart Lock either)
8 | - "browse", "home", album, premium plan, artist and genre screens (some of the blocks might be unsupported)
9 | - library: "liked songs" w/ tag&sort support, rootlist (liked playlists) + pins + artist/album support w/ nice animations, delta updates + pub/sub processing support
10 | - basic playback w/ Spotify Connect support (connect support is very WIP)
11 | - fairly optimized R8 rules, providing __approx. 5-6 megabytes__ release APK size (with the playback and protobuf parts!)
12 |
13 | __What's in progress:__
14 | - "Now Playing" improvements
15 | - better service (notification improvements)
16 |
17 | __Application stack:__
18 | - playback: librespot-java as the core + sinks/decoders from librespot-android + Media2 for the mediasession support
19 | - UI: Jetpack Compose
20 | - DI: Hilt/Dagger
21 | - network: Retrofit w/ Moshi+Protobuf converters
22 | - pictures: Coil
23 | - storage: Room (collection), MMKV (metadata)
24 | - arch: MVVM
25 | - preferences: Jetpack Datastore (proto)
26 |
27 | __Credits:__
28 | - [librespot-java](https://github.com/librespot-org/librespot-java) for the core API part and playback
29 | - [librespot-android](https://github.com/devgianlu/librespot-android) for sink and decoder source (in Jetispot they are rewritten to Kotlin)
30 | - [moshi](https://github.com/square/moshi/) and [moshix](https://github.com/ZacSweers/MoshiX/) for the undocumented API JSON parsing
31 | - [VK Icons](https://github.com/VKCOM/icons) for the amazing icon set used in the application's icon
32 | - [MMKV](https://github.com/Tencent/MMKV) for ultra-fast way to cache entity extended metadata
33 | - Google for Android/Jetpack/Hilt
34 |
35 | _* 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._
36 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/hub/AbsHubViewModel.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.screens.hub
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.lifecycle.ViewModel
6 | import bruhcollective.itaysonlab.jetispot.core.SpPlayerServiceManager
7 | import bruhcollective.itaysonlab.jetispot.core.api.SpPartnersApi
8 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubResponse
9 | import bruhcollective.itaysonlab.jetispot.core.objs.player.PlayFromContextData
10 | import bruhcollective.itaysonlab.jetispot.ui.hub.HubScreenDelegate
11 |
12 | abstract class AbsHubViewModel: ViewModel(), HubScreenDelegate {
13 | private val _state = mutableStateOf(HubState.Loading)
14 | val state: HubState get() = _state.value
15 |
16 | val mainAddedState = mutableStateOf(false)
17 | val imageCache = mutableMapOf()
18 |
19 | val hubTitle get() = (_state.value as? HubState.Loaded)?.data?.title ?: ""
20 |
21 | suspend fun load(loader: suspend () -> HubResponse) {
22 | _state.value = try {
23 | HubState.Loaded(loader())
24 | } catch (e: Exception) {
25 | e.printStackTrace()
26 | HubState.Error(e)
27 | }
28 | }
29 |
30 | suspend fun reload(loader: suspend () -> HubResponse) {
31 | _state.value = HubState.Loading
32 | load(loader)
33 | }
34 |
35 | fun play(spPlayerServiceManager: SpPlayerServiceManager, data: PlayFromContextData) {
36 | spPlayerServiceManager.play(data.uri, data.player)
37 | }
38 |
39 | override fun isSurroundedWithPadding() = false
40 | override fun getMainObjectAddedState() = mainAddedState
41 |
42 | suspend fun calculateDominantColor(partnersApi: SpPartnersApi, url: String, dark: Boolean): Color {
43 | return try {
44 | if (imageCache.containsKey(url)) {
45 | return imageCache[url]!!
46 | }
47 |
48 | val apiResult = partnersApi.fetchExtractedColors(variables = "{\"uris\":[\"$url\"]}").data.extractedColors[0].let {
49 | if (dark) it.colorRaw else it.colorDark
50 | }.hex
51 |
52 | Color(android.graphics.Color.parseColor(apiResult)).also { imageCache[url] = it }
53 | } catch (e: Exception) {
54 | // e.printStackTrace()
55 | Color.Transparent
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/dac/components_home/ToolbarComponentBinder.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.dac.components_home
2 |
3 | import androidx.compose.foundation.layout.WindowInsets
4 | import androidx.compose.foundation.layout.statusBarsPadding
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.rounded.History
7 | import androidx.compose.material.icons.rounded.Notifications
8 | import androidx.compose.material.icons.rounded.Settings
9 | import androidx.compose.material3.*
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.vector.ImageVector
13 | import androidx.compose.ui.unit.dp
14 | import bruhcollective.itaysonlab.jetispot.ui.ext.dynamicUnpack
15 | import bruhcollective.itaysonlab.jetispot.ui.navigation.LocalNavigationController
16 | import com.spotify.home.dac.component.v1.proto.ToolbarComponent
17 | import com.spotify.home.dac.component.v1.proto.ToolbarItemFeedComponent
18 | import com.spotify.home.dac.component.v1.proto.ToolbarItemListeningHistoryComponent
19 | import com.spotify.home.dac.component.v1.proto.ToolbarItemSettingsComponent
20 |
21 | @OptIn(ExperimentalMaterial3Api::class)
22 | @Composable
23 | fun ToolbarComponentBinder(
24 | item: ToolbarComponent
25 | ) {
26 | TopAppBar(title = {
27 | Text(item.dayPartMessage)
28 | }, actions = {
29 | item.itemsList.forEach {
30 | when (val protoItem = it.dynamicUnpack()) {
31 | is ToolbarItemFeedComponent -> ToolbarItem(Icons.Rounded.Notifications, protoItem.navigateUri, protoItem.title)
32 | is ToolbarItemListeningHistoryComponent -> ToolbarItem(Icons.Rounded.History, protoItem.navigateUri, protoItem.title)
33 | is ToolbarItemSettingsComponent -> ToolbarItem(Icons.Rounded.Settings, protoItem.navigateUri, protoItem.title)
34 | }
35 | }
36 | }, modifier = Modifier.statusBarsPadding(), windowInsets = WindowInsets(top = 0.dp))
37 | }
38 |
39 | @Composable
40 | private fun ToolbarItem(
41 | icon: ImageVector,
42 | navigateTo: String,
43 | contentDesc: String
44 | ) {
45 | val navController = LocalNavigationController.current
46 |
47 | IconButton(onClick = {
48 | navController.navigate(navigateTo)
49 | }) {
50 | Icon(icon, contentDescription = contentDesc)
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/monet/ColorToScheme.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.monet
2 |
3 | import androidx.annotation.ColorInt
4 | import androidx.compose.material3.ColorScheme
5 | import androidx.compose.ui.graphics.Color
6 | import bruhcollective.itaysonlab.jetispot.ui.monet.google.scheme.Scheme
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.withContext
9 |
10 | object ColorToScheme {
11 | suspend fun convert(@ColorInt color: Int, dark: Boolean) = withContext(Dispatchers.Default) {
12 | val scheme = if (dark) { Scheme.dark(color) } else { Scheme.light(color) }
13 |
14 | ColorScheme(
15 | primary = scheme.primary.color(),
16 | onPrimary = scheme.onPrimary.color(),
17 | primaryContainer = scheme.primaryContainer.color(),
18 | onPrimaryContainer = scheme.onPrimaryContainer.color(),
19 | inversePrimary = scheme.inversePrimary.color(),
20 | secondary = scheme.secondary.color(),
21 | onSecondary = scheme.onSecondary.color(),
22 | secondaryContainer = scheme.secondaryContainer.color(),
23 | onSecondaryContainer = scheme.onSecondaryContainer.color(),
24 | tertiary = scheme.tertiary.color(),
25 | onTertiary = scheme.onTertiary.color(),
26 | tertiaryContainer = scheme.tertiaryContainer.color(),
27 | onTertiaryContainer = scheme.onTertiaryContainer.color(),
28 | background = scheme.background.color(),
29 | onBackground = scheme.onBackground.color(),
30 | surface = scheme.surface.color(),
31 | onSurface = scheme.onSurface.color(),
32 | surfaceVariant = scheme.surfaceVariant.color(),
33 | onSurfaceVariant = scheme.onSurfaceVariant.color(),
34 | surfaceTint = scheme.primary.color(), // defaults to primary, source: https://github.com/flutter/flutter/pull/100153
35 | inverseSurface = scheme.inverseSurface.color(),
36 | inverseOnSurface = scheme.inverseOnSurface.color(),
37 | error = scheme.error.color(),
38 | onError = scheme.onError.color(),
39 | errorContainer = scheme.errorContainer.color(),
40 | onErrorContainer = scheme.onErrorContainer.color(),
41 | outline = scheme.outline.color(),
42 | outlineVariant = scheme.outlineVariant.color(),
43 | scrim = scheme.scrim.color(),
44 | )
45 | }
46 | }
47 |
48 | private fun Int.color() = Color(this)
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/virt/CollectionEntityView.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.virt
2 |
3 | import bruhcollective.itaysonlab.jetispot.core.SpSessionManager
4 | import bruhcollective.itaysonlab.jetispot.core.collection.db.LocalCollectionDao
5 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.*
6 | import bruhcollective.itaysonlab.jetispot.core.objs.player.*
7 |
8 | object CollectionEntityView {
9 | suspend fun create(
10 | spSessionManager: SpSessionManager,
11 | spDao: LocalCollectionDao,
12 | sort: LocalCollectionDao.TrackSorts,
13 | invert: Boolean,
14 | tag: String?
15 | ): HubResponse {
16 | val uri = "spotify:user:${spSessionManager.session.username()}:collection"
17 |
18 | val hubBody = mutableListOf()
19 | var tags = spDao.getContentFilters()
20 |
21 | if (tag != null) tags = tags.filter { it.query == tag }
22 |
23 | val tracks = spDao.getTracks(tag?.removePrefix("tags contains "), sort, invert)
24 |
25 | tracks.forEach { track ->
26 | hubBody.add(HubItem(
27 | HubComponent.PlaylistTrackRow,
28 | id = track.id,
29 | text = HubText(
30 | title = track.name,
31 | subtitle = track.rawArtistsData.split("|").joinToString { it.split("=").getOrElse(1) { "" } } + " • " + track.albumName
32 | ),
33 | images = HubImages(
34 | main = HubImage(
35 | "https://i.scdn.co/image/${track.picture}"
36 | )
37 | ),
38 | events = HubEvents(
39 | HubEvent.PlayFromContext(
40 | PlayFromContextData(
41 | track.uri,
42 | PlayFromContextPlayerData(
43 | PfcContextData(
44 | url = "context://$uri",
45 | uri = uri
46 | ),
47 | PfcOptions(skip_to = PfcOptSkipTo(track_uri = track.uri))
48 | )
49 | )
50 | )
51 | )
52 | ))
53 | }
54 |
55 | return HubResponse(
56 | header = HubItem(
57 | HubComponent.CollectionHeader,
58 | id = "synth_collectionHeader",
59 | custom = mapOf(
60 | "count" to tracks.size,
61 | "cfr" to tags,
62 | "cfr_cur" to (tag ?: "")
63 | )
64 | ),
65 | body = hubBody
66 | )
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PodcastTopicsStrip.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.lazy.LazyRow
6 | import androidx.compose.foundation.lazy.items
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.rounded.Star
9 | import androidx.compose.material3.ElevatedSuggestionChip
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.remember
15 | import androidx.compose.ui.unit.dp
16 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
17 | import bruhcollective.itaysonlab.jetispot.ui.navigation.LocalNavigationController
18 | import com.spotify.podcastcreatorinteractivity.v1.PodcastRating
19 | import com.spotify.podcastextensions.proto.PodcastTopic
20 | import com.spotify.podcastextensions.proto.PodcastTopics
21 |
22 | @OptIn(ExperimentalMaterial3Api::class)
23 | @Composable
24 | fun PodcastTopicsStrip (
25 | item: HubItem
26 | ) {
27 | val navController = LocalNavigationController.current
28 | val topics = remember { item.custom!!["topics"] as PodcastTopics }
29 | val rating = remember { item.custom!!["ratings"] as PodcastRating }
30 |
31 | LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(horizontal = 16.dp)) {
32 | item {
33 | ElevatedSuggestionChip(onClick = {
34 | // TODO
35 | }, icon = {
36 | Icon(imageVector = Icons.Rounded.Star, contentDescription = null)
37 | }, label = {
38 | Text(text = "${String.format("%.2f", rating.averageRating.average)} (${rating.averageRating.totalRatings})")
39 | })
40 | }
41 |
42 | items(topics.topicsList) { topic ->
43 | PodcastTopic(topic = topic, onClick = navController::navigate)
44 | }
45 | }
46 | }
47 |
48 | @OptIn(ExperimentalMaterial3Api::class)
49 | @Composable
50 | private fun PodcastTopic (
51 | topic: PodcastTopic,
52 | onClick: (String) -> Unit
53 | ) {
54 | ElevatedSuggestionChip(onClick = {
55 | onClick(topic.uri)
56 | }, label = {
57 | Text(text = topic.title)
58 | })
59 | }
--------------------------------------------------------------------------------
/app/src/main/proto/sp/home/ActionCardsComponents.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/any.proto";
4 | import "sp/DacPlayer.proto";
5 |
6 | package com.spotify.home.dac.component.v1.proto;
7 |
8 | option java_multiple_files = true;
9 | option java_package = "com.spotify.home.dac.component.v1.proto";
10 |
11 | message AlbumCardActionsSmallComponent {
12 | string title = 1;
13 | string subtitle = 2;
14 | string navigate_uri = 3;
15 | string like_uri = 4;
16 | string image_uri = 5;
17 | com.spotify.dac.player.v1.proto.PlayCommand play_command = 6;
18 | }
19 |
20 | message ArtistCardActionsSmallComponent {
21 | string title = 1;
22 | string subtitle = 2;
23 | string navigate_uri = 3;
24 | string follow_uri = 4;
25 | string image_uri = 5;
26 | com.spotify.dac.player.v1.proto.PlayCommand play_command = 6;
27 | }
28 |
29 | message PlaylistCardActionsSmallComponent {
30 | string title = 1;
31 | string subtitle = 2;
32 | string navigate_uri = 3;
33 | string like_uri = 4;
34 | string image_uri = 5;
35 | com.spotify.dac.player.v1.proto.PlayCommand play_command = 6;
36 | }
37 |
38 | message AlbumCardActionsMediumComponent {
39 | string title = 1;
40 | string image_uri = 2;
41 | string artist_name = 3;
42 | string artist_image_uri = 4;
43 | string navigate_uri = 5;
44 | string like_uri = 6;
45 | string gradient_color = 7;
46 | string concise_fact = 8;
47 | com.spotify.dac.player.v1.proto.PlayCommand play_command = 9;
48 | google.protobuf.Any context_menu = 10;
49 | string content_type = 11;
50 | string description = 12;
51 | }
52 |
53 | message PlaylistCardActionsMediumComponent {
54 | string title = 1;
55 | string description = 2;
56 | string navigate_uri = 3;
57 | string like_uri = 4;
58 | string image_uri = 5;
59 | string gradient_color = 6;
60 | string concise_fact = 7;
61 | com.spotify.dac.player.v1.proto.PlayCommand play_command = 8;
62 | google.protobuf.Any context_menu = 9;
63 | string content_type = 10;
64 | }
65 |
66 | message ArtistCardActionsMediumComponent {
67 | string title = 1;
68 | string description = 2;
69 | string navigate_uri = 3;
70 | string follow_uri = 4;
71 | string image_uri = 5;
72 | string gradient_color = 6;
73 | com.spotify.dac.player.v1.proto.PlayCommand play_command = 7;
74 | google.protobuf.Any context_menu = 8;
75 | string content_type = 9;
76 | string concise_fact = 10;
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/screens/dynamic/DynamicSpIdScreen.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.screens.dynamic
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import bruhcollective.itaysonlab.jetispot.ui.screens.blend.BlendCreateInvitationScreen
11 | import bruhcollective.itaysonlab.jetispot.ui.screens.config.ConfigScreen
12 | import bruhcollective.itaysonlab.jetispot.ui.screens.history.ListeningHistoryScreen
13 | import bruhcollective.itaysonlab.jetispot.ui.screens.hub.*
14 |
15 | @Composable
16 | fun DynamicSpIdScreen(
17 | uri: String,
18 | fullUri: String,
19 | ) {
20 | var uriSeparated = uri.split(":")
21 | if (uriSeparated[0] == "user" && uriSeparated.size > 2) uriSeparated = uriSeparated.drop(2)
22 | val id = uriSeparated.getOrElse(1) { "" }
23 | val argument = uriSeparated.getOrElse(2) { "" }
24 |
25 | when (uriSeparated[0]) {
26 | "genre" -> BrowseScreen(id)
27 |
28 | "artist" -> HubScreen(
29 | needContentPadding = false,
30 | loader = {
31 | if (argument == "releases") {
32 | getReleasesView(id)
33 | } else {
34 | getArtistView(id)
35 | }
36 | }
37 | )
38 |
39 | "show" -> PodcastShowScreen(id)
40 | "album" -> AlbumScreen(id)
41 | "playlist" -> PlaylistScreen(id)
42 | "config" -> ConfigScreen()
43 | "radio" -> BrowseRadioScreen()
44 |
45 | "collection" -> when (id) {
46 | "artist" -> LikedSongsScreen(
47 | id = argument,
48 | fullUri = fullUri
49 | )
50 | "" -> CollectionScreen()
51 | /* else -> { TODO } */
52 | }
53 |
54 | "internal" -> when (id) {
55 | "listeninghistory" -> ListeningHistoryScreen()
56 | }
57 |
58 | "blend" -> when (id) {
59 | "invitation" -> BlendCreateInvitationScreen()
60 | }
61 |
62 | else -> {
63 | Box(Modifier.fillMaxSize()) {
64 | Column(
65 | modifier = Modifier
66 | .align(Alignment.Center)
67 | ) {
68 | Text(fullUri)
69 | Text(uriSeparated.joinToString(":"))
70 | }
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/bruhcollective/itaysonlab/jetispot/ui/hub/components/PlaylistTrackRow.kt:
--------------------------------------------------------------------------------
1 | package bruhcollective.itaysonlab.jetispot.ui.hub.components
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.text.font.FontWeight
9 | import androidx.compose.ui.unit.dp
10 | import bruhcollective.itaysonlab.jetispot.core.objs.hub.HubItem
11 | import bruhcollective.itaysonlab.jetispot.ui.hub.clickableHub
12 | import bruhcollective.itaysonlab.jetispot.ui.shared.MediumText
13 | import bruhcollective.itaysonlab.jetispot.ui.shared.PreviewableAsyncImage
14 | import bruhcollective.itaysonlab.jetispot.ui.shared.Subtext
15 |
16 | @Composable
17 | fun PlaylistTrackRow(
18 | item: HubItem
19 | ) {
20 | val artists = remember(item) {
21 | if (!item.text?.subtitle.isNullOrEmpty()) {
22 | item.text!!.subtitle!!
23 | } else if (item.custom?.get("artists") != null) {
24 | (item.custom["artists"] as List