Added image loading with coil image cache to HomePage

This commit is contained in:
2026-01-17 17:19:48 +01:00
parent a05497c7bc
commit f979d1b68f
11 changed files with 140 additions and 38 deletions

View File

@@ -65,6 +65,8 @@ dependencies {
implementation(libs.okhttp)
implementation(libs.logging.interceptor)
implementation(libs.androidx.compose.foundation)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -7,13 +7,23 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.lifecycleScope
import coil3.ImageLoader
import coil3.compose.setSingletonImageLoaderFactory
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.crossfade
import coil3.util.DebugLogger
import dagger.hilt.android.AndroidEntryPoint
import hu.bbara.purefin.app.HomePage
import hu.bbara.purefin.client.JellyfinApiClient
import hu.bbara.purefin.client.JellyfinAuthInterceptor
import hu.bbara.purefin.login.ui.LoginScreen
import hu.bbara.purefin.session.UserSessionRepository
import hu.bbara.purefin.ui.theme.PurefinTheme
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okio.Path.Companion.toPath
import javax.inject.Inject
@AndroidEntryPoint
@@ -21,15 +31,48 @@ class PurefinActivity : ComponentActivity() {
@Inject
lateinit var userSessionRepository: UserSessionRepository
@Inject
lateinit var jellyfinApiClient: JellyfinApiClient
@Inject
lateinit var authInterceptor: JellyfinAuthInterceptor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch { init() }
enableEdgeToEdge()
setContent {
PurefinTheme {
setSingletonImageLoaderFactory { context ->
ImageLoader.Builder(context)
.components {
add(
OkHttpNetworkFetcherFactory(
callFactory = {
OkHttpClient.Builder()
.addNetworkInterceptor(authInterceptor)
.build()
}
)
)
}
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(context, 0.10)
.build()
}
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache").absolutePath.toPath())
.maxSizeBytes(30000000)
.build()
}
.logger(DebugLogger())
.crossfade(true)
.build()
}
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false)
if (isLoggedIn) {
HomePage()

View File

@@ -6,4 +6,6 @@ import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class PurefinApplication : Application() {
}

View File

@@ -46,13 +46,15 @@ class HomePageViewModel @Inject constructor(
_continueWatching.value = continueWatching.map {
if (it.type == BaseItemKind.EPISODE) {
ContinueWatchingItem(
id = it.id,
primaryText = it.seriesName!!,
secondaryText = it.name!!,
progress = it.userData!!.playedPercentage!!.toFloat(),
colors = listOf(Color.Red, Color.Green)
colors = listOf(Color.Red, Color.Green),
)
} else {
ContinueWatchingItem(
id = it.id,
primaryText = it.name!!,
secondaryText = it.premiereDate!!.format(DateTimeFormatter.ofLocalizedDate(
FormatStyle.MEDIUM)),
@@ -86,6 +88,7 @@ class HomePageViewModel @Inject constructor(
val libraryItems = jellyfinApiClient.getLibrary(libraryId)
val posterItems = libraryItems.map {
PosterItem(
id = it.id,
title = it.name ?: "Unknown",
colors = listOf(Color.Blue, Color.Cyan),
isLatest = false

View File

@@ -5,6 +5,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import org.jellyfin.sdk.model.UUID
data class ContinueWatchingItem(
val id: UUID,
val primaryText: String,
val secondaryText: String,
val progress: Float,
@@ -17,6 +18,7 @@ data class LibraryItem(
)
data class PosterItem(
val id: UUID,
val title: String,
val isLatest: Boolean,
val colors: List<Color>

View File

@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -23,12 +24,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import hu.bbara.purefin.image.JellyfinImageHelper
import org.jellyfin.sdk.model.api.ImageType
@Composable
fun ContinueWatchingSection(
@@ -73,10 +77,15 @@ fun ContinueWatchingCard(
.clip(RoundedCornerShape(16.dp))
.background(colors.card)
) {
Box(
modifier = Modifier
.matchParentSize()
.background(Brush.linearGradient(item.colors))
AsyncImage(
model = JellyfinImageHelper.toImageUrl(
url = "https://jellyfin.bbara.hu",
itemId = item.id,
type = ImageType.PRIMARY
),
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
Box(
modifier = Modifier
@@ -159,36 +168,11 @@ fun PosterCard(
.clip(RoundedCornerShape(14.dp))
.background(colors.card)
) {
Box(
modifier = Modifier
.matchParentSize()
.background(Brush.linearGradient(item.colors))
)
if (item.isLatest) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
.clip(RoundedCornerShape(6.dp))
.background(colors.primary)
.padding(horizontal = 8.dp, vertical = 2.dp)
) {
Text(
text = "LATEST",
color = colors.onPrimary,
fontSize = 10.sp,
fontWeight = FontWeight.Bold
)
}
}
Box(
modifier = Modifier
.matchParentSize()
.background(
Brush.verticalGradient(
listOf(Color.Transparent, Color.Black.copy(alpha = 0.85f))
)
)
AsyncImage(
model = JellyfinImageHelper.toImageUrl(url = "https://jellyfin.bbara.hu", itemId = item.id, type = ImageType.PRIMARY),
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
Text(
text = item.title,

View File

@@ -64,8 +64,7 @@ class JellyfinApiClient @Inject constructor(
userId = userId,
startIndex = 0,
//TODO remove this limit if needed
limit = 10,
enableImages = false,
limit = 10
)
val response: Response<BaseItemDtoQueryResult> = api.itemsApi.getResumeItems(getResumeItemsRequest)
Log.d("getContinueWatching response: {}", response.content.toString())

View File

@@ -0,0 +1,22 @@
package hu.bbara.purefin.client
import hu.bbara.purefin.session.UserSessionRepository
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
class JellyfinAuthInterceptor @Inject constructor (
private val userSessionRepository: UserSessionRepository
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = runBlocking { userSessionRepository.accessToken.first() }
val request = chain.request().newBuilder()
.addHeader("X-Emby-Token", token)
// Some Jellyfin versions prefer the Authorization header:
// .addHeader("Authorization", "MediaBrowser Client=\"YourAppName\", Device=\"YourDevice\", DeviceId=\"123\", Version=\"1.0.0\", Token=\"$token\"")
.build()
return chain.proceed(request)
}
}

View File

@@ -0,0 +1,22 @@
package hu.bbara.purefin.image
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import hu.bbara.purefin.client.JellyfinAuthInterceptor
import okhttp3.OkHttpClient
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object ImageModule {
@Provides
@Singleton
fun provideOkHttpClient(authInterceptor: JellyfinAuthInterceptor): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.build()
}
}

View File

@@ -0,0 +1,18 @@
package hu.bbara.purefin.image
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.ImageType
class JellyfinImageHelper {
companion object {
fun toImageUrl(url: String, itemId: UUID, type: ImageType): String {
return StringBuilder()
.append(url)
.append("/Items/")
.append(itemId)
.append("/Images/")
.append(type.serialName)
.toString()
}
}
}