mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Basic home page & images
This commit is contained in:
parent
76d78246db
commit
51ad92f539
16 changed files with 421 additions and 42 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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<UserPreferences>
|
||||
|
||||
@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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
) {
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DolphinModel>,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class MainViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
val api: ApiClient,
|
||||
) : ViewModel() {
|
||||
val homeRows = MutableLiveData<List<HomeRow>>()
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,14 @@
|
|||
<resources>
|
||||
<string name="app_name">Dolphin</string>
|
||||
|
||||
<string name="home_section_latest_media">Recently added media</string>
|
||||
<string name="home_section_library">My media</string>
|
||||
<string name="home_section_library_small">My media (small)</string>
|
||||
<string name="home_section_resume">Continue watching</string>
|
||||
<string name="home_section_resume_audio">Continue listening</string>
|
||||
<string name="home_section_resume_book">Continue reading</string>
|
||||
<string name="home_section_active_recordings">Active recordings</string>
|
||||
<string name="home_section_next_up">Next up</string>
|
||||
<string name="home_section_livetv">Live TV</string>
|
||||
<string name="home_section_none">None</string>
|
||||
</resources>
|
||||
|
|
@ -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" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue