mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
Added image loading with coil image cache to HomePage
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -6,4 +6,6 @@ import dagger.hilt.android.HiltAndroidApp
|
||||
@HiltAndroidApp
|
||||
class PurefinApplication : Application() {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
22
app/src/main/java/hu/bbara/purefin/image/ImageModule.kt
Normal file
22
app/src/main/java/hu/bbara/purefin/image/ImageModule.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ datastore = "1.1.1"
|
||||
kotlinxSerializationJson = "1.7.3"
|
||||
okhttp = "4.12.0"
|
||||
foundation = "1.10.1"
|
||||
coil = "3.3.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -43,6 +44,10 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
||||
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
|
||||
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
|
||||
coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }
|
||||
|
||||
|
||||
|
||||
|
||||
[plugins]
|
||||
|
||||
Reference in New Issue
Block a user