mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Optimizes recommended content & image caching (#359)
## Details This PR contains a number of optimizations ### Images Updates to request images with maximum sizes The max sizes are estimates on the maximum number of pixels that will be actually displayed. This means the server is downscaling large images which means 1) better quality downscaling and 2) smaller images sent over the network and cached This should mean slightly better quality images and more images that can be cached locally ### Paging Reduces network calls when fetching paginated data, ie grid pages and some rows. This PR combines the total record count & first page queries which slightly reduces total time to load paginated data. Additionally, on movies/tv show recommended pages, the rows now only fetch a limited number of items (same as number on home page). While they previously fetched using pagination, there is an overhead required to paginate requests, so this is slightly faster. ### Async recommended fetching On movies/tv show recommended pages, the rows are now fetched completely separately from each other. Previously, the rows were requested separtely, but each was only added to the UI in order as each completed. Now they will add as the data arrived. Note: Top unwatched TV shows is still very slow on `10.11.x`, see #204 ### Image cache The local, on-disk image cache size is increased from 100mb to 200mb. Combined with the smaller images, this covers about 1500-2000 poster images, but will vary by server. The cache size is now also configurable in advanced settings from 25mb to 1000mb. Additionally, you can see how much of both the disk and memory caches are in use. ## Issues Closes #330 Related to #120
This commit is contained in:
parent
10233eb459
commit
56bf3cb7a0
16 changed files with 453 additions and 253 deletions
|
|
@ -19,6 +19,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -27,6 +28,8 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Surface
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
|
|
@ -85,9 +88,31 @@ class MainActivity : AppCompatActivity() {
|
|||
appUpgradeHandler.copySubfont(false)
|
||||
}
|
||||
setContent {
|
||||
CoilConfig(okHttpClient, false)
|
||||
val density = LocalDensity.current
|
||||
LaunchedEffect(density) {
|
||||
with(density) {
|
||||
// Cards are never taller than 200 (most are around 120)
|
||||
BaseItem.primaryMaxHeight = 200.dp.roundToPx()
|
||||
// This width covers up to 2.35:1 aspect ratio images
|
||||
BaseItem.primaryMaxWidth = 480.dp.roundToPx()
|
||||
}
|
||||
}
|
||||
|
||||
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
|
||||
appPreferences?.let { appPreferences ->
|
||||
CoilConfig(
|
||||
diskCacheSizeBytes =
|
||||
appPreferences.advancedPreferences.imageDiskCacheSizeBytes.let {
|
||||
if (it < AppPreference.ImageDiskCacheSize.min * AppPreference.MEGA_BIT) {
|
||||
AppPreference.ImageDiskCacheSize.defaultValue * AppPreference.MEGA_BIT
|
||||
} else {
|
||||
it
|
||||
}
|
||||
},
|
||||
okHttpClient = okHttpClient,
|
||||
debugLogging = false,
|
||||
enableCache = true,
|
||||
)
|
||||
LaunchedEffect(appPreferences.debugLogging) {
|
||||
DebugLogTree.INSTANCE.enabled = appPreferences.debugLogging
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import org.jellyfin.sdk.model.api.BaseItemDto
|
|||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Serializable
|
||||
|
|
@ -94,6 +95,17 @@ data class BaseItem(
|
|||
}
|
||||
|
||||
companion object {
|
||||
var primaryMaxWidth: Int? = null
|
||||
set(value) {
|
||||
Timber.v("primaryMaxWidth=$value")
|
||||
field = value
|
||||
}
|
||||
var primaryMaxHeight: Int? = null
|
||||
set(value) {
|
||||
Timber.v("primaryMaxHeight=$value")
|
||||
field = value
|
||||
}
|
||||
|
||||
fun from(
|
||||
dto: BaseItemDto,
|
||||
api: ApiClient,
|
||||
|
|
@ -111,18 +123,25 @@ data class BaseItem(
|
|||
api.imageApi.getItemImageUrl(dto.id, ImageType.BACKDROP)
|
||||
}
|
||||
val primaryImageUrl =
|
||||
if (useSeriesForPrimary && dto.type == BaseItemKind.EPISODE) {
|
||||
val seriesId = dto.seriesId
|
||||
if (seriesId != null) {
|
||||
api.imageApi.getItemImageUrl(seriesId, ImageType.PRIMARY)
|
||||
} else {
|
||||
api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY)
|
||||
}
|
||||
if (useSeriesForPrimary && dto.type == BaseItemKind.EPISODE && dto.seriesId != null) {
|
||||
api.imageApi.getItemImageUrl(
|
||||
dto.seriesId!!,
|
||||
ImageType.PRIMARY,
|
||||
maxHeight = primaryMaxHeight,
|
||||
maxWidth = primaryMaxWidth,
|
||||
quality = 96,
|
||||
)
|
||||
} else if (dto.imageTags == null || dto.imageTags!![ImageType.PRIMARY] == null) {
|
||||
// TODO is this a bad assumption?
|
||||
null
|
||||
} else {
|
||||
api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY)
|
||||
api.imageApi.getItemImageUrl(
|
||||
dto.id,
|
||||
ImageType.PRIMARY,
|
||||
maxHeight = primaryMaxHeight,
|
||||
maxWidth = primaryMaxWidth,
|
||||
quality = 96,
|
||||
)
|
||||
}
|
||||
val logoImageUrl =
|
||||
if (dto.type == BaseItemKind.EPISODE || dto.type == BaseItemKind.SEASON) {
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ sealed interface AppPreference<T> {
|
|||
},
|
||||
)
|
||||
|
||||
private const val MEGA_BIT = 1024 * 1024L
|
||||
const val MEGA_BIT = 1024 * 1024L
|
||||
const val DEFAULT_BITRATE = 100 * MEGA_BIT
|
||||
private val bitrateValues =
|
||||
listOf(
|
||||
|
|
@ -713,6 +713,30 @@ sealed interface AppPreference<T> {
|
|||
summaryOn = R.string.enabled,
|
||||
summaryOff = R.string.disabled,
|
||||
)
|
||||
|
||||
val ImageDiskCacheSize =
|
||||
AppSliderPreference(
|
||||
title = R.string.image_cache_size,
|
||||
defaultValue = 200,
|
||||
min = 25,
|
||||
max = 1_000,
|
||||
interval = 25,
|
||||
getter = {
|
||||
it.advancedPreferences.imageDiskCacheSizeBytes / MEGA_BIT
|
||||
},
|
||||
setter = { prefs, value ->
|
||||
prefs.updateAdvancedPreferences {
|
||||
imageDiskCacheSizeBytes = value * MEGA_BIT
|
||||
}
|
||||
},
|
||||
summarizer = { value ->
|
||||
if (value != null) {
|
||||
"$value MB"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -852,6 +876,7 @@ val advancedPreferences =
|
|||
AppPreference.SendAppLogs,
|
||||
AppPreference.SendCrashReports,
|
||||
AppPreference.DebugLogging,
|
||||
AppPreference.ImageDiskCacheSize,
|
||||
AppPreference.ClearImageCache,
|
||||
AppPreference.OssLicenseInfo,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -95,6 +95,14 @@ class AppPreferencesSerializer
|
|||
resetSubtitles()
|
||||
}.build()
|
||||
}.build()
|
||||
|
||||
advancedPreferences =
|
||||
AdvancedPreferences
|
||||
.newBuilder()
|
||||
.apply {
|
||||
imageDiskCacheSizeBytes =
|
||||
AppPreference.ImageDiskCacheSize.defaultValue * AppPreference.MEGA_BIT
|
||||
}.build()
|
||||
}.build()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): AppPreferences {
|
||||
|
|
@ -143,6 +151,11 @@ inline fun AppPreferences.updateSubtitlePreferences(block: SubtitlePreferences.B
|
|||
subtitlesPreferences = subtitlesPreferences.toBuilder().apply(block).build()
|
||||
}
|
||||
|
||||
inline fun AppPreferences.updateAdvancedPreferences(block: AdvancedPreferences.Builder.() -> Unit): AppPreferences =
|
||||
update {
|
||||
advancedPreferences = advancedPreferences.toBuilder().apply(block).build()
|
||||
}
|
||||
|
||||
fun SubtitlePreferences.Builder.resetSubtitles() {
|
||||
fontSize = SubtitleSettings.FontSize.defaultValue.toInt()
|
||||
fontColor = SubtitleSettings.FontColor.defaultValue.toArgb()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.preference.PreferenceManager
|
|||
import com.github.damontecres.wholphin.WholphinApplication
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences
|
||||
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
|
||||
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
|
||||
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
||||
|
|
@ -133,4 +134,14 @@ suspend fun upgradeApp(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (previous.isEqualOrBefore(Version.fromString("0.3.4"))) {
|
||||
appPreferences.updateData {
|
||||
it.updateAdvancedPreferences {
|
||||
if (imageDiskCacheSizeBytes < (AppPreference.ImageDiskCacheSize.min * AppPreference.MEGA_BIT)) {
|
||||
imageDiskCacheSizeBytes =
|
||||
AppPreference.ImageDiskCacheSize.defaultValue * AppPreference.MEGA_BIT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.github.damontecres.wholphin.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import coil3.ImageLoader
|
||||
import coil3.annotation.ExperimentalCoilApi
|
||||
import coil3.compose.setSingletonImageLoaderFactory
|
||||
|
|
@ -12,6 +13,7 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
|||
import coil3.request.crossfade
|
||||
import coil3.util.DebugLogger
|
||||
import okhttp3.OkHttpClient
|
||||
import timber.log.Timber
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
/**
|
||||
|
|
@ -20,26 +22,53 @@ import kotlin.time.ExperimentalTime
|
|||
@OptIn(ExperimentalTime::class, ExperimentalCoilApi::class)
|
||||
@Composable
|
||||
fun CoilConfig(
|
||||
diskCacheSizeBytes: Long,
|
||||
okHttpClient: OkHttpClient,
|
||||
debugLogging: Boolean,
|
||||
enableCache: Boolean = true,
|
||||
) {
|
||||
val client =
|
||||
remember(okHttpClient, debugLogging) {
|
||||
if (debugLogging) {
|
||||
okHttpClient
|
||||
.newBuilder()
|
||||
.addInterceptor {
|
||||
val start = System.currentTimeMillis()
|
||||
val req = it.request()
|
||||
val res = it.proceed(req)
|
||||
val time = System.currentTimeMillis() - start
|
||||
Timber.v("${time}ms - ${req.url}")
|
||||
res
|
||||
}.build()
|
||||
} else {
|
||||
okHttpClient
|
||||
}
|
||||
}
|
||||
setSingletonImageLoaderFactory { ctx ->
|
||||
Timber.i("Image diskCacheSizeBytes=$diskCacheSizeBytes")
|
||||
ImageLoader
|
||||
.Builder(ctx)
|
||||
.memoryCache(MemoryCache.Builder().maxSizePercent(ctx).build())
|
||||
.diskCache(
|
||||
DiskCache
|
||||
.Builder()
|
||||
.directory(ctx.cacheDir.resolve("coil3_image_cache"))
|
||||
.maxSizeBytes(100L * 1024 * 1024)
|
||||
.build(),
|
||||
).crossfade(false)
|
||||
.apply {
|
||||
if (enableCache) {
|
||||
memoryCache(MemoryCache.Builder().maxSizePercent(ctx).build())
|
||||
diskCache(
|
||||
DiskCache
|
||||
.Builder()
|
||||
.directory(ctx.cacheDir.resolve("coil3_image_cache"))
|
||||
.maxSizeBytes(diskCacheSizeBytes)
|
||||
.build(),
|
||||
)
|
||||
} else {
|
||||
memoryCache(null)
|
||||
diskCache(null)
|
||||
}
|
||||
}.crossfade(false)
|
||||
.logger(if (debugLogging) DebugLogger() else null)
|
||||
.components {
|
||||
add(
|
||||
OkHttpNetworkFetcherFactory(
|
||||
cacheStrategy = { CacheControlCacheStrategy() },
|
||||
callFactory = { okHttpClient },
|
||||
callFactory = { client },
|
||||
),
|
||||
)
|
||||
}.build()
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.media3.common.Player
|
||||
import coil3.request.ErrorResult
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumnSaver
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
|
|
@ -39,7 +40,10 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.acra.ACRA
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.Response
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
|
@ -392,3 +396,8 @@ fun logTab(
|
|||
}
|
||||
|
||||
suspend fun <T> onMain(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
|
||||
|
||||
fun Response<BaseItemDtoQueryResult>.toBaseItems(
|
||||
api: ApiClient,
|
||||
useSeriesForPrimary: Boolean,
|
||||
) = this.content.items.map { BaseItem.from(it, api, useSeriesForPrimary) }
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ fun BannerCard(
|
|||
played: Boolean = false,
|
||||
favorite: Boolean = false,
|
||||
playPercent: Double = 0.0,
|
||||
cardHeight: Dp = 140.dp * .85f,
|
||||
cardHeight: Dp = 120.dp,
|
||||
aspectRatio: Float = AspectRatios.WIDE,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ fun ChapterCard(
|
|||
onClick: () -> Unit,
|
||||
aspectRatio: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
cardHeight: Dp = 140.dp * .85f,
|
||||
cardHeight: Dp = 120.dp,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -32,11 +34,14 @@ import com.github.damontecres.wholphin.ui.main.HomePageContent
|
|||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import java.util.UUID
|
||||
|
||||
abstract class RecommendedViewModel(
|
||||
val context: Context,
|
||||
val navigationManager: NavigationManager,
|
||||
val favoriteWatchManager: FavoriteWatchManager,
|
||||
) : ViewModel() {
|
||||
|
|
@ -79,6 +84,27 @@ abstract class RecommendedViewModel(
|
|||
refreshItem(position, itemId)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun update(
|
||||
@StringRes title: Int,
|
||||
row: HomeRowLoadingState,
|
||||
)
|
||||
|
||||
fun update(
|
||||
@StringRes title: Int,
|
||||
block: suspend () -> List<BaseItem>,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val titleStr = context.getString(title)
|
||||
val row =
|
||||
try {
|
||||
HomeRowLoadingState.Success(titleStr, block.invoke())
|
||||
} catch (ex: Exception) {
|
||||
HomeRowLoadingState.Error(titleStr, null, ex)
|
||||
}
|
||||
update(title, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
import com.github.damontecres.wholphin.ui.toBaseItems
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
|
||||
|
|
@ -26,9 +30,8 @@ import dagger.assisted.AssistedInject
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -46,13 +49,14 @@ import java.util.UUID
|
|||
class RecommendedMovieViewModel
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
@ApplicationContext context: Context,
|
||||
private val api: ApiClient,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val preferencesDataStore: DataStore<AppPreferences>,
|
||||
@Assisted val parentId: UUID,
|
||||
navigationManager: NavigationManager,
|
||||
favoriteWatchManager: FavoriteWatchManager,
|
||||
) : RecommendedViewModel(navigationManager, favoriteWatchManager) {
|
||||
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(parentId: UUID): RecommendedMovieViewModel
|
||||
|
|
@ -60,16 +64,21 @@ class RecommendedMovieViewModel
|
|||
|
||||
override val rows =
|
||||
MutableStateFlow<List<HomeRowLoadingState>>(
|
||||
rowTitles
|
||||
.map {
|
||||
HomeRowLoadingState.Pending(
|
||||
context.getString(it),
|
||||
)
|
||||
},
|
||||
rowTitles.keys.map {
|
||||
HomeRowLoadingState.Pending(
|
||||
context.getString(it),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
override fun init() {
|
||||
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||
val itemsPerRow =
|
||||
preferencesDataStore.data
|
||||
.firstOrNull()
|
||||
?.homePagePreferences
|
||||
?.maxItemsPerRow
|
||||
?: AppPreference.HomePageItems.defaultValue.toInt()
|
||||
try {
|
||||
val resumeItemsRequest =
|
||||
GetResumeItemsRequest(
|
||||
|
|
@ -77,20 +86,16 @@ class RecommendedMovieViewModel
|
|||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
enableUserData = true,
|
||||
startIndex = 0,
|
||||
limit = itemsPerRow,
|
||||
enableTotalRecordCount = false,
|
||||
)
|
||||
val resumeItems =
|
||||
ApiRequestPager(
|
||||
api,
|
||||
resumeItemsRequest,
|
||||
GetResumeItemsRequestHandler,
|
||||
viewModelScope,
|
||||
).init()
|
||||
if (resumeItems.isNotEmpty()) {
|
||||
resumeItems.getBlocking(0)
|
||||
}
|
||||
|
||||
GetResumeItemsRequestHandler
|
||||
.execute(api, resumeItemsRequest)
|
||||
.toBaseItems(api, false)
|
||||
update(
|
||||
0,
|
||||
R.string.continue_watching,
|
||||
HomeRowLoadingState.Success(
|
||||
context.getString(R.string.continue_watching),
|
||||
resumeItems,
|
||||
|
|
@ -107,95 +112,90 @@ class RecommendedMovieViewModel
|
|||
}
|
||||
}
|
||||
|
||||
val recentlyReleasedRequest =
|
||||
GetItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
recursive = true,
|
||||
enableUserData = true,
|
||||
sortBy = listOf(ItemSortBy.PREMIERE_DATE),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
)
|
||||
val recentlyReleasedItems =
|
||||
ApiRequestPager(api, recentlyReleasedRequest, GetItemsRequestHandler, viewModelScope)
|
||||
update(R.string.recently_released) {
|
||||
val recentlyReleasedRequest =
|
||||
GetItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
recursive = true,
|
||||
enableUserData = true,
|
||||
sortBy = listOf(ItemSortBy.PREMIERE_DATE),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
startIndex = 0,
|
||||
limit = itemsPerRow,
|
||||
enableTotalRecordCount = false,
|
||||
)
|
||||
GetItemsRequestHandler
|
||||
.execute(api, recentlyReleasedRequest)
|
||||
.toBaseItems(api, false)
|
||||
}
|
||||
|
||||
val recentlyAddedRequest =
|
||||
GetItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
recursive = true,
|
||||
enableUserData = true,
|
||||
sortBy = listOf(ItemSortBy.DATE_CREATED),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
)
|
||||
val recentlyAddedItems =
|
||||
ApiRequestPager(api, recentlyAddedRequest, GetItemsRequestHandler, viewModelScope)
|
||||
update(R.string.recently_added) {
|
||||
val recentlyAddedRequest =
|
||||
GetItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
recursive = true,
|
||||
enableUserData = true,
|
||||
sortBy = listOf(ItemSortBy.DATE_CREATED),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
startIndex = 0,
|
||||
limit = itemsPerRow,
|
||||
enableTotalRecordCount = false,
|
||||
)
|
||||
GetItemsRequestHandler
|
||||
.execute(api, recentlyAddedRequest)
|
||||
.toBaseItems(api, false)
|
||||
}
|
||||
|
||||
val suggestionsRequest =
|
||||
GetSuggestionsRequest(
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
type = listOf(BaseItemKind.MOVIE),
|
||||
)
|
||||
val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope)
|
||||
update(R.string.suggestions) {
|
||||
val suggestionsRequest =
|
||||
GetSuggestionsRequest(
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
type = listOf(BaseItemKind.MOVIE),
|
||||
startIndex = 0,
|
||||
limit = itemsPerRow,
|
||||
enableTotalRecordCount = false,
|
||||
)
|
||||
GetSuggestionsRequestHandler
|
||||
.execute(api, suggestionsRequest)
|
||||
.toBaseItems(api, false)
|
||||
}
|
||||
|
||||
val unwatchedTopRatedRequest =
|
||||
GetItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
recursive = true,
|
||||
enableUserData = true,
|
||||
isPlayed = false,
|
||||
sortBy = listOf(ItemSortBy.COMMUNITY_RATING),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
)
|
||||
val unwatchedTopRatedItems =
|
||||
ApiRequestPager(api, unwatchedTopRatedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true)
|
||||
update(R.string.top_unwatched) {
|
||||
val unwatchedTopRatedRequest =
|
||||
GetItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
recursive = true,
|
||||
enableUserData = true,
|
||||
isPlayed = false,
|
||||
sortBy = listOf(ItemSortBy.COMMUNITY_RATING),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
startIndex = 0,
|
||||
limit = itemsPerRow,
|
||||
enableTotalRecordCount = false,
|
||||
)
|
||||
GetItemsRequestHandler
|
||||
.execute(api, unwatchedTopRatedRequest)
|
||||
.toBaseItems(api, false)
|
||||
}
|
||||
|
||||
val rows =
|
||||
listOf(
|
||||
R.string.recently_released to recentlyReleasedItems,
|
||||
R.string.recently_added to recentlyAddedItems,
|
||||
R.string.suggestions to suggestedItems,
|
||||
R.string.top_unwatched to unwatchedTopRatedItems,
|
||||
)
|
||||
rows
|
||||
.mapIndexed { index, (title, pager) ->
|
||||
viewModelScope.async(Dispatchers.IO + ExceptionHandler()) {
|
||||
val title = context.getString(title)
|
||||
val result =
|
||||
try {
|
||||
pager.init()
|
||||
if (pager.isNotEmpty()) {
|
||||
pager.getBlocking(0)
|
||||
}
|
||||
HomeRowLoadingState.Success(title, pager)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error fetching %s", title)
|
||||
HomeRowLoadingState.Error(title, null, ex)
|
||||
}
|
||||
update(index + 1, result)
|
||||
if (result is HomeRowLoadingState.Success && result.items.isNotEmpty() &&
|
||||
(loading.value == LoadingState.Loading || loading.value == LoadingState.Pending)
|
||||
) {
|
||||
loading.setValueOnMain(LoadingState.Success)
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) {
|
||||
loading.setValueOnMain(LoadingState.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(
|
||||
position: Int,
|
||||
override fun update(
|
||||
@StringRes title: Int,
|
||||
row: HomeRowLoadingState,
|
||||
) {
|
||||
rows.update { current ->
|
||||
current.toMutableList().apply { set(position, row) }
|
||||
current.toMutableList().apply { set(rowTitles[title]!!, row) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -207,7 +207,7 @@ class RecommendedMovieViewModel
|
|||
R.string.recently_added,
|
||||
R.string.suggestions,
|
||||
R.string.top_unwatched,
|
||||
)
|
||||
).mapIndexed { index, i -> i to index }.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
import com.github.damontecres.wholphin.ui.toBaseItems
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.GetNextUpRequestHandler
|
||||
|
|
@ -28,8 +32,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -48,13 +52,14 @@ import java.util.UUID
|
|||
class RecommendedTvShowViewModel
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
@ApplicationContext context: Context,
|
||||
private val api: ApiClient,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val preferencesDataStore: DataStore<AppPreferences>,
|
||||
@Assisted val parentId: UUID,
|
||||
navigationManager: NavigationManager,
|
||||
favoriteWatchManager: FavoriteWatchManager,
|
||||
) : RecommendedViewModel(navigationManager, favoriteWatchManager) {
|
||||
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(parentId: UUID): RecommendedTvShowViewModel
|
||||
|
|
@ -62,63 +67,66 @@ class RecommendedTvShowViewModel
|
|||
|
||||
override val rows =
|
||||
MutableStateFlow<List<HomeRowLoadingState>>(
|
||||
rowTitles
|
||||
.map {
|
||||
HomeRowLoadingState.Pending(
|
||||
context.getString(it),
|
||||
)
|
||||
},
|
||||
rowTitles.keys.map {
|
||||
HomeRowLoadingState.Pending(
|
||||
context.getString(it),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
override fun init() {
|
||||
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||
val itemsPerRow =
|
||||
preferencesDataStore.data
|
||||
.firstOrNull()
|
||||
?.homePagePreferences
|
||||
?.maxItemsPerRow
|
||||
?: AppPreference.HomePageItems.defaultValue.toInt()
|
||||
try {
|
||||
val resumeItemsRequest =
|
||||
GetResumeItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||
enableUserData = true,
|
||||
)
|
||||
val resumeItems =
|
||||
ApiRequestPager(
|
||||
api,
|
||||
resumeItemsRequest,
|
||||
GetResumeItemsRequestHandler,
|
||||
viewModelScope,
|
||||
useSeriesForPrimary = true,
|
||||
).init()
|
||||
if (resumeItems.isNotEmpty()) {
|
||||
resumeItems.getBlocking(0)
|
||||
}
|
||||
val resumeItemsDeferred =
|
||||
viewModelScope.async {
|
||||
val resumeItemsRequest =
|
||||
GetResumeItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||
enableUserData = true,
|
||||
startIndex = 0,
|
||||
limit = itemsPerRow,
|
||||
enableTotalRecordCount = false,
|
||||
)
|
||||
GetResumeItemsRequestHandler
|
||||
.execute(api, resumeItemsRequest)
|
||||
.toBaseItems(api, true)
|
||||
}
|
||||
|
||||
val nextUpRequest =
|
||||
GetNextUpRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
enableUserData = true,
|
||||
)
|
||||
val nextUpItems =
|
||||
ApiRequestPager(
|
||||
api,
|
||||
nextUpRequest,
|
||||
GetNextUpRequestHandler,
|
||||
viewModelScope,
|
||||
useSeriesForPrimary = true,
|
||||
).init()
|
||||
if (nextUpItems.isNotEmpty()) {
|
||||
nextUpItems.getBlocking(0)
|
||||
}
|
||||
val nextUpItemsDeferred =
|
||||
viewModelScope.async {
|
||||
val nextUpRequest =
|
||||
GetNextUpRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
enableUserData = true,
|
||||
startIndex = 0,
|
||||
limit = itemsPerRow,
|
||||
enableTotalRecordCount = false,
|
||||
)
|
||||
|
||||
GetNextUpRequestHandler
|
||||
.execute(api, nextUpRequest)
|
||||
.toBaseItems(api, true)
|
||||
}
|
||||
val resumeItems = resumeItemsDeferred.await()
|
||||
update(
|
||||
0,
|
||||
R.string.continue_watching,
|
||||
HomeRowLoadingState.Success(
|
||||
context.getString(R.string.continue_watching),
|
||||
resumeItems,
|
||||
),
|
||||
)
|
||||
val nextUpItems = nextUpItemsDeferred.await()
|
||||
update(
|
||||
1,
|
||||
R.string.next_up,
|
||||
HomeRowLoadingState.Success(
|
||||
context.getString(R.string.next_up),
|
||||
nextUpItems,
|
||||
|
|
@ -135,96 +143,93 @@ class RecommendedTvShowViewModel
|
|||
}
|
||||
}
|
||||
|
||||
val recentlyReleasedRequest =
|
||||
GetItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||
recursive = true,
|
||||
enableUserData = true,
|
||||
sortBy = listOf(ItemSortBy.PREMIERE_DATE),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
)
|
||||
val recentlyReleasedItems =
|
||||
ApiRequestPager(api, recentlyReleasedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true)
|
||||
update(R.string.recently_released) {
|
||||
val recentlyReleasedRequest =
|
||||
GetItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||
recursive = true,
|
||||
enableUserData = true,
|
||||
sortBy = listOf(ItemSortBy.PREMIERE_DATE),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
startIndex = 0,
|
||||
limit = itemsPerRow,
|
||||
enableTotalRecordCount = false,
|
||||
)
|
||||
|
||||
val recentlyAddedRequest =
|
||||
GetItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||
recursive = true,
|
||||
enableUserData = true,
|
||||
sortBy = listOf(ItemSortBy.DATE_CREATED),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
)
|
||||
val recentlyAddedItems =
|
||||
ApiRequestPager(api, recentlyAddedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true)
|
||||
GetItemsRequestHandler
|
||||
.execute(api, recentlyReleasedRequest)
|
||||
.toBaseItems(api, true)
|
||||
}
|
||||
|
||||
val suggestionsRequest =
|
||||
GetSuggestionsRequest(
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
type = listOf(BaseItemKind.SERIES),
|
||||
)
|
||||
val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope)
|
||||
update(R.string.recently_added) {
|
||||
val recentlyAddedRequest =
|
||||
GetItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||
recursive = true,
|
||||
enableUserData = true,
|
||||
sortBy = listOf(ItemSortBy.DATE_CREATED),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
startIndex = 0,
|
||||
limit = itemsPerRow,
|
||||
enableTotalRecordCount = false,
|
||||
)
|
||||
|
||||
val unwatchedTopRatedRequest =
|
||||
GetItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.SERIES),
|
||||
recursive = true,
|
||||
enableUserData = true,
|
||||
isPlayed = false,
|
||||
sortBy = listOf(ItemSortBy.COMMUNITY_RATING),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
)
|
||||
val unwatchedTopRatedItems =
|
||||
ApiRequestPager(api, unwatchedTopRatedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true)
|
||||
GetItemsRequestHandler
|
||||
.execute(api, recentlyAddedRequest)
|
||||
.toBaseItems(api, true)
|
||||
}
|
||||
|
||||
val rows =
|
||||
listOf(
|
||||
R.string.recently_released to recentlyReleasedItems,
|
||||
R.string.recently_added to recentlyAddedItems,
|
||||
R.string.suggestions to suggestedItems,
|
||||
R.string.top_unwatched to unwatchedTopRatedItems,
|
||||
)
|
||||
update(R.string.suggestions) {
|
||||
val suggestionsRequest =
|
||||
GetSuggestionsRequest(
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
type = listOf(BaseItemKind.SERIES),
|
||||
startIndex = 0,
|
||||
limit = itemsPerRow,
|
||||
enableTotalRecordCount = false,
|
||||
)
|
||||
|
||||
GetSuggestionsRequestHandler
|
||||
.execute(api, suggestionsRequest)
|
||||
.toBaseItems(api, true)
|
||||
}
|
||||
|
||||
update(R.string.top_unwatched) {
|
||||
val unwatchedTopRatedRequest =
|
||||
GetItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.SERIES),
|
||||
recursive = true,
|
||||
enableUserData = true,
|
||||
isPlayed = false,
|
||||
sortBy = listOf(ItemSortBy.COMMUNITY_RATING),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
startIndex = 0,
|
||||
limit = itemsPerRow,
|
||||
enableTotalRecordCount = false,
|
||||
)
|
||||
GetItemsRequestHandler
|
||||
.execute(api, unwatchedTopRatedRequest)
|
||||
.toBaseItems(api, true)
|
||||
}
|
||||
|
||||
rows
|
||||
.mapIndexed { index, (title, pager) ->
|
||||
viewModelScope.async(Dispatchers.IO + ExceptionHandler()) {
|
||||
val title = context.getString(title)
|
||||
val result =
|
||||
try {
|
||||
pager.init()
|
||||
if (pager.isNotEmpty()) {
|
||||
pager.getBlocking(0)
|
||||
}
|
||||
HomeRowLoadingState.Success(title, pager)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error fetching %s", title)
|
||||
HomeRowLoadingState.Error(title, null, ex)
|
||||
}
|
||||
update(index + 2, result)
|
||||
if (result is HomeRowLoadingState.Success && result.items.isNotEmpty() &&
|
||||
(loading.value == LoadingState.Loading || loading.value == LoadingState.Pending)
|
||||
) {
|
||||
loading.setValueOnMain(LoadingState.Success)
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) {
|
||||
loading.setValueOnMain(LoadingState.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(
|
||||
position: Int,
|
||||
override fun update(
|
||||
@StringRes title: Int,
|
||||
row: HomeRowLoadingState,
|
||||
) {
|
||||
rows.update { current ->
|
||||
current.toMutableList().apply { set(position, row) }
|
||||
current.toMutableList().apply { set(rowTitles[title]!!, row) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -237,7 +242,7 @@ class RecommendedTvShowViewModel
|
|||
R.string.recently_added,
|
||||
R.string.suggestions,
|
||||
R.string.top_unwatched,
|
||||
)
|
||||
).mapIndexed { index, i -> i to index }.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import androidx.tv.material3.MaterialTheme
|
|||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.imageLoader
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
|
|
@ -79,12 +80,21 @@ fun PreferencesContent(
|
|||
var preferences by remember { mutableStateOf(initialPreferences) }
|
||||
|
||||
val navDrawerPins by viewModel.navDrawerPins.observeAsState(mapOf())
|
||||
var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.preferenceDataStore.data.collect {
|
||||
preferences = it
|
||||
}
|
||||
}
|
||||
var updateCache by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(updateCache) {
|
||||
val imageUsedMemory = context.imageLoader.memoryCache?.size ?: 0L
|
||||
val imageMaxMemory = context.imageLoader.memoryCache?.maxSize ?: 0L
|
||||
val imageDisk = context.imageLoader.diskCache?.size ?: 0L
|
||||
cacheUsage = CacheUsage(imageUsedMemory, imageMaxMemory, imageDisk)
|
||||
updateCache = false
|
||||
}
|
||||
|
||||
val release by updateVM.release.observeAsState(null)
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
@ -284,16 +294,28 @@ fun PreferencesContent(
|
|||
}
|
||||
|
||||
AppPreference.ClearImageCache -> {
|
||||
val summary =
|
||||
remember(cacheUsage) {
|
||||
cacheUsage.let {
|
||||
val diskMB = it.imageDiskUsed / AppPreference.MEGA_BIT
|
||||
val memoryUsedMB =
|
||||
it.imageMemoryUsed / AppPreference.MEGA_BIT
|
||||
val memoryMaxMB =
|
||||
it.imageMemoryMax / AppPreference.MEGA_BIT
|
||||
"Disk: ${diskMB}mb, Memory: ${memoryUsedMB}mb/${memoryMaxMB}mb"
|
||||
}
|
||||
}
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
SingletonImageLoader.get(context).let {
|
||||
it.memoryCache?.clear()
|
||||
it.diskCache?.clear()
|
||||
updateCache = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary = null,
|
||||
summary = summary,
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
|
|
@ -419,3 +441,9 @@ fun PreferencesPage(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CacheUsage(
|
||||
val imageMemoryUsed: Long,
|
||||
val imageMemoryMax: Long,
|
||||
val imageDiskUsed: Long,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -64,11 +64,9 @@ class ApiRequestPager<T>(
|
|||
.maximumSize(cacheSize)
|
||||
.build<Int, MutableList<BaseItem>>()
|
||||
|
||||
suspend fun init(): ApiRequestPager<T> {
|
||||
suspend fun init(initialPosition: Int = 0): ApiRequestPager<T> {
|
||||
if (totalCount < 0) {
|
||||
val newRequest = requestHandler.prepare(request, 0, 1, true)
|
||||
val result = requestHandler.execute(api, newRequest).content
|
||||
totalCount = result.totalRecordCount
|
||||
fetchPage(initialPosition, true).join()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
|
@ -77,7 +75,7 @@ class ApiRequestPager<T>(
|
|||
if (index in 0..<totalCount) {
|
||||
val item = items[index]
|
||||
if (item == null) {
|
||||
fetchPage(index)
|
||||
fetchPage(index, false)
|
||||
}
|
||||
return item
|
||||
} else {
|
||||
|
|
@ -89,7 +87,7 @@ class ApiRequestPager<T>(
|
|||
if (index in 0..<totalCount) {
|
||||
val item = items[index]
|
||||
if (item == null) {
|
||||
fetchPage(index).join()
|
||||
fetchPage(index, false).join()
|
||||
return items[index]
|
||||
}
|
||||
return item
|
||||
|
|
@ -112,7 +110,10 @@ class ApiRequestPager<T>(
|
|||
override val size: Int
|
||||
get() = totalCount
|
||||
|
||||
private fun fetchPage(position: Int): Job =
|
||||
private fun fetchPage(
|
||||
position: Int,
|
||||
setTotalCount: Boolean,
|
||||
): Job =
|
||||
scope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||
mutex.withLock {
|
||||
val pageNumber = position / pageSize
|
||||
|
|
@ -123,9 +124,12 @@ class ApiRequestPager<T>(
|
|||
request,
|
||||
pageNumber * pageSize,
|
||||
pageSize,
|
||||
false,
|
||||
setTotalCount,
|
||||
)
|
||||
val result = requestHandler.execute(api, newRequest).content
|
||||
if (setTotalCount) {
|
||||
totalCount = result.totalRecordCount
|
||||
}
|
||||
val data = mutableListOf<BaseItem>()
|
||||
result.items.forEach { data.add(BaseItem.from(it, api, useSeriesForPrimary)) }
|
||||
cachedPages.put(pageNumber, data)
|
||||
|
|
|
|||
|
|
@ -132,6 +132,10 @@ message InterfacePreferences {
|
|||
SubtitlePreferences subtitles_preferences = 7;
|
||||
}
|
||||
|
||||
message AdvancedPreferences {
|
||||
int64 image_disk_cache_size_bytes = 1;
|
||||
}
|
||||
|
||||
message AppPreferences {
|
||||
// The currently signed in server and user IDs, mostly for restoring a session
|
||||
string current_server_id = 1;
|
||||
|
|
@ -145,4 +149,5 @@ message AppPreferences {
|
|||
string update_url = 7;
|
||||
bool send_crash_reports = 8;
|
||||
bool debug_logging = 9;
|
||||
AdvancedPreferences advanced_preferences = 10;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,6 +328,7 @@
|
|||
<string name="dolby_atmos">Dolby Atmos</string>
|
||||
<string name="subtitle_margin">Margin</string>
|
||||
<string name="verbose_logging">Verbose logging</string>
|
||||
<string name="image_cache_size">Image disk cache size (MB)</string>
|
||||
|
||||
<string-array name="theme_song_volume">
|
||||
<item>Disabled</item>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue