From 51ad92f539206b1f9920185b5abe67417b35a487 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sun, 21 Sep 2025 22:16:42 -0400 Subject: [PATCH] Basic home page & images --- app/build.gradle.kts | 4 + .../github/damontecres/dolphin/CoilConfig.kt | 56 ++++++ .../damontecres/dolphin/DolphinApplication.kt | 4 + .../github/damontecres/dolphin/Extensions.kt | 12 ++ .../damontecres/dolphin/MainActivity.kt | 14 +- .../dolphin/data/ServerRepository.kt | 3 + .../dolphin/data/model/DolphinModel.kt | 21 +++ .../damontecres/dolphin/data/model/Video.kt | 25 +++ .../damontecres/dolphin/hilt/DolphinModule.kt | 5 + .../github/damontecres/dolphin/ui/MainPage.kt | 39 ---- .../dolphin/ui/cards/DolphinCard.kt | 23 +++ .../damontecres/dolphin/ui/cards/VideoCard.kt | 38 ++++ .../dolphin/ui/main/HomeSection.kt | 30 ++++ .../damontecres/dolphin/ui/main/MainPage.kt | 169 ++++++++++++++++++ app/src/main/res/values/strings.xml | 11 ++ gradle/libs.versions.toml | 9 +- 16 files changed, 421 insertions(+), 42 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/dolphin/CoilConfig.kt create mode 100644 app/src/main/java/com/github/damontecres/dolphin/Extensions.kt create mode 100644 app/src/main/java/com/github/damontecres/dolphin/data/model/DolphinModel.kt create mode 100644 app/src/main/java/com/github/damontecres/dolphin/data/model/Video.kt delete mode 100644 app/src/main/java/com/github/damontecres/dolphin/ui/MainPage.kt create mode 100644 app/src/main/java/com/github/damontecres/dolphin/ui/cards/DolphinCard.kt create mode 100644 app/src/main/java/com/github/damontecres/dolphin/ui/cards/VideoCard.kt create mode 100644 app/src/main/java/com/github/damontecres/dolphin/ui/main/HomeSection.kt create mode 100644 app/src/main/java/com/github/damontecres/dolphin/ui/main/MainPage.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 166793fa..cc2863cc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -76,6 +76,8 @@ dependencies { implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.runtime.livedata) implementation(libs.androidx.tv.foundation) implementation(libs.androidx.tv.material) implementation(libs.androidx.lifecycle.runtime.ktx) @@ -113,6 +115,8 @@ dependencies { ksp(libs.androidx.room.compiler) ksp(libs.hilt.android.compiler) + implementation(libs.timber) + androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) diff --git a/app/src/main/java/com/github/damontecres/dolphin/CoilConfig.kt b/app/src/main/java/com/github/damontecres/dolphin/CoilConfig.kt new file mode 100644 index 00000000..a577c8fa --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/CoilConfig.kt @@ -0,0 +1,56 @@ +package com.github.damontecres.dolphin + +import androidx.compose.runtime.Composable +import coil3.ImageLoader +import coil3.annotation.ExperimentalCoilApi +import coil3.compose.setSingletonImageLoaderFactory +import coil3.disk.DiskCache +import coil3.disk.directory +import coil3.network.cachecontrol.CacheControlCacheStrategy +import coil3.network.okhttp.OkHttpNetworkFetcherFactory +import coil3.request.crossfade +import coil3.util.DebugLogger +import com.github.damontecres.dolphin.data.ServerRepository +import okhttp3.Call +import okhttp3.OkHttpClient +import kotlin.time.ExperimentalTime + +@OptIn(ExperimentalTime::class, ExperimentalCoilApi::class) +@Composable +fun CoilConfig( + serverRepository: ServerRepository, + okHttpClient: OkHttpClient, + debugLogging: Boolean, +) { + setSingletonImageLoaderFactory { ctx -> + ImageLoader + .Builder(ctx) + .diskCache( + DiskCache + .Builder() + .directory(ctx.cacheDir.resolve("coil3_image_cache")) + .maxSizeBytes(100L * 1024 * 1024) + .build(), + ).crossfade(true) + .logger(if (debugLogging) DebugLogger() else null) + .components { + add( + OkHttpNetworkFetcherFactory( + cacheStrategy = { CacheControlCacheStrategy() }, + callFactory = { + Call.Factory { request -> + // Ref: https://gist.github.com/nielsvanvelzen/ea047d9028f676185832e51ffaf12a6f + val token = serverRepository.currentUser?.accessToken + okHttpClient.newCall( + request + .newBuilder() + .addHeader("Authorization", "MediaBrowser Token=\"$token\"") + .build(), + ) + } + }, + ), + ) + }.build() + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/DolphinApplication.kt b/app/src/main/java/com/github/damontecres/dolphin/DolphinApplication.kt index bb8490b7..67977f1f 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/DolphinApplication.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/DolphinApplication.kt @@ -2,11 +2,15 @@ package com.github.damontecres.dolphin import android.app.Application import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber @HiltAndroidApp class DolphinApplication : Application() { init { instance = this + + // TODO only plant in debug builds + Timber.plant(Timber.DebugTree()) } companion object { diff --git a/app/src/main/java/com/github/damontecres/dolphin/Extensions.kt b/app/src/main/java/com/github/damontecres/dolphin/Extensions.kt new file mode 100644 index 00000000..0fb80ed8 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/Extensions.kt @@ -0,0 +1,12 @@ +package com.github.damontecres.dolphin + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +@OptIn(ExperimentalContracts::class) +fun CharSequence?.isNotNullOrBlank(): Boolean { + contract { + returns(true) implies (this@isNotNullOrBlank != null) + } + return !this.isNullOrBlank() +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/dolphin/MainActivity.kt index 919e21b7..187498ab 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/MainActivity.kt @@ -12,13 +12,14 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.datastore.core.DataStore import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Surface -import androidx.tv.material3.Text import com.github.damontecres.dolphin.data.ServerRepository import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.ui.ServerLoginPage +import com.github.damontecres.dolphin.ui.main.MainPage import com.github.damontecres.dolphin.ui.theme.DolphinTheme import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import okhttp3.OkHttpClient import javax.inject.Inject @AndroidEntryPoint @@ -29,6 +30,9 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var userPreferencesDataStore: DataStore + @Inject + lateinit var okHttpClient: OkHttpClient + @OptIn(ExperimentalTvMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -39,6 +43,8 @@ class MainActivity : AppCompatActivity() { modifier = Modifier.fillMaxSize(), shape = RectangleShape, ) { + CoilConfig(serverRepository, okHttpClient, false) + val preferences by userPreferencesDataStore.data.collectAsState(null) preferences?.let { preferences -> if (preferences.currentServerId.isNotBlank() && preferences.currentUserId.isNotBlank()) { @@ -54,7 +60,11 @@ class MainActivity : AppCompatActivity() { val server = serverRepository.currentServer val user = serverRepository.currentUser if (server != null && user != null) { - Text("Logged in as ${user.name} on ${server.url}") + // TODO navigation + MainPage( + preferences = preferences, + modifier = Modifier.fillMaxSize(), + ) } } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/data/ServerRepository.kt b/app/src/main/java/com/github/damontecres/dolphin/data/ServerRepository.kt index c20886a4..f9682525 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/data/ServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/data/ServerRepository.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.exception.InvalidStatusException import org.jellyfin.sdk.api.client.extensions.userApi +import timber.log.Timber import javax.inject.Inject class ServerRepository @@ -51,6 +52,7 @@ class ServerRepository server: JellyfinServer, user: JellyfinUser, ) { + Timber.v("Changing user to ${user.name} on ${server.url}") apiClient.update(baseUrl = server.url, accessToken = user.accessToken) try { apiClient.userApi @@ -60,6 +62,7 @@ class ServerRepository _currentUser = user } catch (e: InvalidStatusException) { // TODO + Timber.e(e) if (e.status == 401) { // Unauthorized _currentServer = null diff --git a/app/src/main/java/com/github/damontecres/dolphin/data/model/DolphinModel.kt b/app/src/main/java/com/github/damontecres/dolphin/data/model/DolphinModel.kt new file mode 100644 index 00000000..e4d814f4 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/data/model/DolphinModel.kt @@ -0,0 +1,21 @@ +package com.github.damontecres.dolphin.data.model + +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.model.api.BaseItemDto +import org.jellyfin.sdk.model.api.BaseItemKind +import java.util.UUID + +sealed interface DolphinModel { + val id: UUID + val name: String? + val imageUrl: String? +} + +fun convertModel( + dto: BaseItemDto, + api: ApiClient, +): DolphinModel = + when (dto.type) { + BaseItemKind.VIDEO -> Video.fromDto(dto, api) + else -> throw IllegalArgumentException("Unsupported item type: ${dto.type}") + } diff --git a/app/src/main/java/com/github/damontecres/dolphin/data/model/Video.kt b/app/src/main/java/com/github/damontecres/dolphin/data/model/Video.kt new file mode 100644 index 00000000..dcdcd016 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/data/model/Video.kt @@ -0,0 +1,25 @@ +package com.github.damontecres.dolphin.data.model + +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.imageApi +import org.jellyfin.sdk.model.api.BaseItemDto +import org.jellyfin.sdk.model.api.ImageType +import java.util.UUID + +data class Video( + override val id: UUID, + override val name: String?, + override val imageUrl: String?, +) : DolphinModel { + companion object { + fun fromDto( + dto: BaseItemDto, + api: ApiClient, + ): Video = + Video( + id = dto.id, + name = dto.name, + imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY), + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/hilt/DolphinModule.kt b/app/src/main/java/com/github/damontecres/dolphin/hilt/DolphinModule.kt index b54ae779..09f11a86 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/hilt/DolphinModule.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/hilt/DolphinModule.kt @@ -15,11 +15,13 @@ import org.jellyfin.sdk.api.okhttp.OkHttpFactory import org.jellyfin.sdk.createJellyfin import org.jellyfin.sdk.model.ClientInfo import org.jellyfin.sdk.model.DeviceInfo +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object DolphinModule { @Provides + @Singleton fun okHttpClient() = OkHttpClient .Builder() @@ -28,9 +30,11 @@ object DolphinModule { }.build() @Provides + @Singleton fun okHttpFactory(okHttpClient: OkHttpClient) = OkHttpFactory(okHttpClient) @Provides + @Singleton fun jellyfin( okHttpFactory: OkHttpFactory, @ApplicationContext context: Context, @@ -53,5 +57,6 @@ object DolphinModule { } @Provides + @Singleton fun apiClient(jellyfin: Jellyfin) = jellyfin.createApi() } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/MainPage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/MainPage.kt deleted file mode 100644 index fe7f144f..00000000 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/MainPage.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.github.damontecres.dolphin.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel -import com.github.damontecres.dolphin.preferences.UserPreferences -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.userApi -import javax.inject.Inject - -@HiltViewModel -class MainViewModel - @Inject - constructor( - val api: ApiClient, - ) : ViewModel() { - init { - viewModelScope.launch { - api.userApi.getCurrentUser().content.configuration?.let { - it.orderedViews - } - } - } - - fun todo() { - } - } - -@Composable -fun MainPage( - preferences: UserPreferences, - modifier: Modifier, - viewModel: MainViewModel = viewModel(), -) { -} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/DolphinCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/DolphinCard.kt new file mode 100644 index 00000000..308b14d6 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/DolphinCard.kt @@ -0,0 +1,23 @@ +package com.github.damontecres.dolphin.ui.cards + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.github.damontecres.dolphin.data.model.DolphinModel +import com.github.damontecres.dolphin.data.model.Video + +@Composable +fun DolphinCard( + item: DolphinModel?, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + when (item) { + is Video -> + VideoCard( + item = item, + onClick = onClick, + modifier = modifier, + ) + null -> TODO() + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/VideoCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/VideoCard.kt new file mode 100644 index 00000000..4eb4e706 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/VideoCard.kt @@ -0,0 +1,38 @@ +package com.github.damontecres.dolphin.ui.cards + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Card +import androidx.tv.material3.Text +import coil3.compose.AsyncImage +import com.github.damontecres.dolphin.data.model.Video + +@Composable +fun VideoCard( + item: Video, + onClick: () -> Unit, + modifier: Modifier = Modifier, + cardWidth: Dp = 150.dp, + cardHeight: Dp = 200.dp, +) { + Card( + modifier = modifier, + onClick = onClick, + ) { + Column( + modifier = Modifier.size(cardWidth, cardHeight), + ) { + Text( + text = item.name ?: "", + ) + AsyncImage( + model = item.imageUrl, + contentDescription = item.name, + ) + } + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomeSection.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomeSection.kt new file mode 100644 index 00000000..e65cda5d --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomeSection.kt @@ -0,0 +1,30 @@ +package com.github.damontecres.dolphin.ui.main + +import androidx.annotation.StringRes +import com.github.damontecres.dolphin.R + +/** + * All possible homesections, "synced" with jellyfin-web. + * + * https://github.com/jellyfin/jellyfin-web/blob/master/src/components/homesections/homesections.js + */ +enum class HomeSection( + val key: String, + @param:StringRes val nameRes: Int, +) { + LATEST_MEDIA("latestmedia", R.string.home_section_latest_media), + LIBRARY_TILES_SMALL("smalllibrarytiles", R.string.home_section_library), + LIBRARY_BUTTONS("librarybuttons", R.string.home_section_library_small), + RESUME("resume", R.string.home_section_resume), + RESUME_AUDIO("resumeaudio", R.string.home_section_resume_audio), + RESUME_BOOK("resumebook", R.string.home_section_resume_book), + ACTIVE_RECORDINGS("activerecordings", R.string.home_section_active_recordings), + NEXT_UP("nextup", R.string.home_section_next_up), + LIVE_TV("livetv", R.string.home_section_livetv), + NONE("none", R.string.home_section_none), + ; + + companion object { + fun fromKey(key: String): HomeSection = entries.firstOrNull { it.key == key } ?: NONE + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainPage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainPage.kt new file mode 100644 index 00000000..61928665 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainPage.kt @@ -0,0 +1,169 @@ +package com.github.damontecres.dolphin.ui.main + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.tv.material3.Text +import com.github.damontecres.dolphin.data.model.DolphinModel +import com.github.damontecres.dolphin.data.model.convertModel +import com.github.damontecres.dolphin.isNotNullOrBlank +import com.github.damontecres.dolphin.preferences.UserPreferences +import com.github.damontecres.dolphin.ui.cards.DolphinCard +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi +import org.jellyfin.sdk.api.client.extensions.userApi +import org.jellyfin.sdk.api.client.extensions.userLibraryApi +import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest +import timber.log.Timber +import javax.inject.Inject + +data class HomeRow( + val section: HomeSection, + val items: List, +) + +@HiltViewModel +class MainViewModel + @Inject + constructor( + val api: ApiClient, + ) : ViewModel() { + val homeRows = MutableLiveData>() + + init { + viewModelScope.launch(Dispatchers.IO) { + val user = api.userApi.getCurrentUser().content + val displayPrefs = + api.displayPreferencesApi + .getDisplayPreferences( + displayPreferencesId = "usersettings", + client = "emby", + ).content + val homeSections = + displayPrefs.customPrefs.entries + .filter { it.key.startsWith("homesection") && it.value.isNotNullOrBlank() } + .sortedBy { it.key } + .map { HomeSection.fromKey(it.value ?: "") } + .filterNot { it == HomeSection.NONE } + + val homeRows = + homeSections + .mapNotNull { section -> + Timber.v("Loading section: %s", section.name) + when (section) { + HomeSection.LATEST_MEDIA -> { + user.configuration?.orderedViews?.firstOrNull()?.let { viewId -> + val request = + GetLatestMediaRequest( + fields = listOf(), + imageTypeLimit = 1, + parentId = viewId, + groupItems = true, + limit = 25, + ) + val latest = + api.userLibraryApi + .getLatestMedia(request) + .content + .map { convertModel(it, api) } + Timber.v("latest: %s", latest) + // api.itemsApi.getItems(request) + HomeRow( + section = section, + items = latest, + ) + } + } + + // TODO + HomeSection.LIBRARY_TILES_SMALL -> null + HomeSection.RESUME -> null + HomeSection.ACTIVE_RECORDINGS -> null + HomeSection.NEXT_UP -> null + HomeSection.LIVE_TV -> null + + // TODO Not supported? + HomeSection.LIBRARY_BUTTONS -> null + HomeSection.RESUME_AUDIO -> null + HomeSection.RESUME_BOOK -> null + HomeSection.NONE -> null + } + }.filter { it.items.isNotEmpty() } + withContext(Dispatchers.Main) { + this@MainViewModel.homeRows.value = homeRows + } + } + } + } + +@Composable +fun MainPage( + preferences: UserPreferences, + modifier: Modifier, + viewModel: MainViewModel = viewModel(), +) { + val homeRows by viewModel.homeRows.observeAsState(listOf()) + Column(modifier = modifier) { + // TODO header? + LazyColumn { + homeRows.forEach { row -> + item { + HomePageRow( + row = row, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + } +} + +@Composable +fun HomePageRow( + row: HomeRow, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier, + ) { + Text( + text = stringResource(row.section.nameRes), + ) + LazyRow( + horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(8.dp), + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxWidth(), + ) { + items(row.items) { item -> + DolphinCard( + item = item, + onClick = {}, + modifier = Modifier, + ) + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e08d44c3..9e7d7e3f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,14 @@ Dolphin + + Recently added media + My media + My media (small) + Continue watching + Continue listening + Continue reading + Active recordings + Next up + Live TV + None \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 23910cdc..f9a34f11 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,8 @@ ksp = "2.2.20-2.0.2" # https://github.com/google/ksp/issues/2596 2.0.3 is broken coreKtx = "1.17.0" appcompat = "1.7.1" composeBom = "2025.09.00" +compose-runtime = "1.9.1" +timber = "5.0.1" tvFoundation = "1.0.0-alpha12" tvMaterial = "1.0.1" lifecycleRuntimeKtx = "2.9.4" @@ -34,6 +36,10 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } +androidx-compose-runtime = { module = "androidx.compose.runtime:runtime-android", version.ref = "compose-runtime" } +androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "compose-runtime" } + androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "tvFoundation" } androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "tvMaterial" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } @@ -71,7 +77,8 @@ androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3. androidx-room-common-jvm = { group = "androidx.room", name = "room-common-jvm", version.ref = "room" } androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } -androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } + +timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }