mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat: Implement Library feature with storage, repository, and UI components
This commit is contained in:
parent
bb00cde10a
commit
7f7f7e2bae
16 changed files with 465 additions and 2 deletions
|
|
@ -5,6 +5,7 @@ import androidx.activity.ComponentActivity
|
|||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import com.nuvio.app.features.addons.AddonStorage
|
||||
import com.nuvio.app.features.library.LibraryStorage
|
||||
import com.nuvio.app.features.home.HomeCatalogSettingsStorage
|
||||
import com.nuvio.app.features.player.PlayerSettingsStorage
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesStorage
|
||||
|
|
@ -15,6 +16,7 @@ class MainActivity : ComponentActivity() {
|
|||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
AddonStorage.initialize(applicationContext)
|
||||
LibraryStorage.initialize(applicationContext)
|
||||
HomeCatalogSettingsStorage.initialize(applicationContext)
|
||||
PlayerSettingsStorage.initialize(applicationContext)
|
||||
ContinueWatchingPreferencesStorage.initialize(applicationContext)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
actual object LibraryClock {
|
||||
actual fun nowEpochMs(): Long = System.currentTimeMillis()
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
actual object LibraryStorage {
|
||||
private const val preferencesName = "nuvio_library"
|
||||
private const val payloadKey = "library_payload"
|
||||
|
||||
private var preferences: SharedPreferences? = null
|
||||
|
||||
fun initialize(context: Context) {
|
||||
preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
preferences?.getString(payloadKey, null)
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.putString(payloadKey, payload)
|
||||
?.apply()
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.VideoLibrary
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -40,6 +41,8 @@ import com.nuvio.app.features.details.MetaDetailsScreen
|
|||
import com.nuvio.app.features.home.HomeCatalogSection
|
||||
import com.nuvio.app.features.home.HomeScreen
|
||||
import com.nuvio.app.features.home.MetaPreview
|
||||
import com.nuvio.app.features.library.LibraryItem
|
||||
import com.nuvio.app.features.library.LibraryScreen
|
||||
import com.nuvio.app.features.player.PlayerRoute
|
||||
import com.nuvio.app.features.player.PlayerScreen
|
||||
import com.nuvio.app.features.search.SearchScreen
|
||||
|
|
@ -86,6 +89,7 @@ data class CatalogRoute(
|
|||
enum class AppScreenTab {
|
||||
Home,
|
||||
Search,
|
||||
Library,
|
||||
Settings,
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +99,7 @@ fun AppScreen(
|
|||
modifier: Modifier = Modifier,
|
||||
onCatalogClick: ((HomeCatalogSection) -> Unit)? = null,
|
||||
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
||||
onLibraryPosterClick: ((LibraryItem) -> Unit)? = null,
|
||||
onContinueWatchingClick: ((ContinueWatchingItem) -> Unit)? = null,
|
||||
onContinueWatchingLongPress: ((ContinueWatchingItem) -> Unit)? = null,
|
||||
) {
|
||||
|
|
@ -110,6 +115,10 @@ fun AppScreen(
|
|||
modifier = modifier,
|
||||
onPosterClick = onPosterClick,
|
||||
)
|
||||
AppScreenTab.Library -> LibraryScreen(
|
||||
modifier = modifier,
|
||||
onPosterClick = onLibraryPosterClick,
|
||||
)
|
||||
AppScreenTab.Settings -> SettingsScreen(
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
@ -227,6 +236,13 @@ fun App() {
|
|||
label = { Text("Search") },
|
||||
colors = navigationItemColors,
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == AppScreenTab.Library,
|
||||
onClick = { selectedTab = AppScreenTab.Library },
|
||||
icon = { Icon(Icons.Rounded.VideoLibrary, contentDescription = null) },
|
||||
label = { Text("Library") },
|
||||
colors = navigationItemColors,
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == AppScreenTab.Settings,
|
||||
onClick = { selectedTab = AppScreenTab.Settings },
|
||||
|
|
@ -246,6 +262,9 @@ fun App() {
|
|||
onPosterClick = { meta ->
|
||||
navController.navigate(DetailRoute(type = meta.type, id = meta.id))
|
||||
},
|
||||
onLibraryPosterClick = { item ->
|
||||
navController.navigate(DetailRoute(type = item.type, id = item.id))
|
||||
},
|
||||
onContinueWatchingClick = onContinueWatchingClick,
|
||||
onContinueWatchingLongPress = onContinueWatchingLongPress,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -93,11 +95,18 @@ fun NuvioPosterCard(
|
|||
shape: NuvioPosterShape = NuvioPosterShape.Poster,
|
||||
detailLine: String? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(110.dp)
|
||||
.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier),
|
||||
.then(
|
||||
if (onClick != null || onLongClick != null) {
|
||||
Modifier.posterCardClickable(onClick = onClick, onLongClick = onLongClick)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Box(
|
||||
|
|
@ -235,3 +244,13 @@ private val NuvioPosterShape.aspectRatio: Float
|
|||
NuvioPosterShape.Square -> 1f
|
||||
NuvioPosterShape.Landscape -> 1.77f
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun Modifier.posterCardClickable(
|
||||
onClick: (() -> Unit)?,
|
||||
onLongClick: (() -> Unit)?,
|
||||
): Modifier =
|
||||
combinedClickable(
|
||||
onClick = { onClick?.invoke() },
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ import com.nuvio.app.features.details.components.DetailCastSection
|
|||
import com.nuvio.app.features.details.components.DetailHero
|
||||
import com.nuvio.app.features.details.components.DetailMetaInfo
|
||||
import com.nuvio.app.features.details.components.DetailSeriesContent
|
||||
import com.nuvio.app.features.library.LibraryRepository
|
||||
import com.nuvio.app.features.library.toLibraryItem
|
||||
import com.nuvio.app.features.watchprogress.WatchProgressEntry
|
||||
import com.nuvio.app.features.watchprogress.WatchProgressRepository
|
||||
import com.nuvio.app.features.watchprogress.buildPlaybackVideoId
|
||||
|
|
@ -56,6 +58,10 @@ fun MetaDetailsScreen(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val uiState by MetaDetailsRepository.uiState.collectAsStateWithLifecycle()
|
||||
val libraryUiState by remember {
|
||||
LibraryRepository.ensureLoaded()
|
||||
LibraryRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val watchProgressUiState by remember {
|
||||
WatchProgressRepository.ensureLoaded()
|
||||
WatchProgressRepository.uiState
|
||||
|
|
@ -114,6 +120,9 @@ fun MetaDetailsScreen(
|
|||
|
||||
requestedMeta != null -> {
|
||||
val meta = requestedMeta
|
||||
val isSaved = remember(libraryUiState.items, meta.id) {
|
||||
libraryUiState.items.any { it.id == meta.id }
|
||||
}
|
||||
val movieProgress = watchProgressUiState.byVideoId[meta.id]
|
||||
val seriesResumeEntry = watchProgressUiState.entries
|
||||
.filter { it.parentMetaId == meta.id }
|
||||
|
|
@ -146,6 +155,7 @@ fun MetaDetailsScreen(
|
|||
) {
|
||||
DetailActionButtons(
|
||||
playLabel = playButtonLabel,
|
||||
saveLabel = if (isSaved) "Saved" else "Save",
|
||||
onPlayClick = {
|
||||
when {
|
||||
meta.type == "series" && seriesResumeEntry != null -> {
|
||||
|
|
@ -208,6 +218,11 @@ fun MetaDetailsScreen(
|
|||
}
|
||||
}
|
||||
},
|
||||
onSaveClick = {
|
||||
LibraryRepository.toggleSaved(
|
||||
meta.toLibraryItem(savedAtEpochMs = 0L),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
DetailMetaInfo(meta = meta)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp
|
|||
fun DetailActionButtons(
|
||||
modifier: Modifier = Modifier,
|
||||
playLabel: String = "Play",
|
||||
saveLabel: String = "Save",
|
||||
onPlayClick: () -> Unit = {},
|
||||
onSaveClick: () -> Unit = {},
|
||||
) {
|
||||
|
|
@ -73,7 +74,7 @@ fun DetailActionButtons(
|
|||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "Save",
|
||||
text = saveLabel,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ fun HomePosterCard(
|
|||
item: MetaPreview,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
) {
|
||||
NuvioPosterCard(
|
||||
title = item.name,
|
||||
|
|
@ -20,6 +21,7 @@ fun HomePosterCard(
|
|||
shape = item.posterShape.toNuvioPosterShape(),
|
||||
detailLine = item.releaseInfo,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
internal expect object LibraryClock {
|
||||
fun nowEpochMs(): Long
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
import com.nuvio.app.features.details.MetaDetails
|
||||
import com.nuvio.app.features.home.MetaPreview
|
||||
import com.nuvio.app.features.home.PosterShape
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LibraryItem(
|
||||
val id: String,
|
||||
val type: String,
|
||||
val name: String,
|
||||
val poster: String? = null,
|
||||
val banner: String? = null,
|
||||
val logo: String? = null,
|
||||
val description: String? = null,
|
||||
val releaseInfo: String? = null,
|
||||
val imdbRating: String? = null,
|
||||
val genres: List<String> = emptyList(),
|
||||
val posterShape: PosterShape = PosterShape.Poster,
|
||||
val savedAtEpochMs: Long,
|
||||
)
|
||||
|
||||
data class LibrarySection(
|
||||
val type: String,
|
||||
val displayTitle: String,
|
||||
val items: List<LibraryItem>,
|
||||
)
|
||||
|
||||
data class LibraryUiState(
|
||||
val items: List<LibraryItem> = emptyList(),
|
||||
val sections: List<LibrarySection> = emptyList(),
|
||||
val isLoaded: Boolean = false,
|
||||
)
|
||||
|
||||
fun MetaDetails.toLibraryItem(savedAtEpochMs: Long): LibraryItem =
|
||||
LibraryItem(
|
||||
id = id,
|
||||
type = type,
|
||||
name = name,
|
||||
poster = poster,
|
||||
banner = background,
|
||||
logo = logo,
|
||||
description = description,
|
||||
releaseInfo = releaseInfo,
|
||||
imdbRating = imdbRating,
|
||||
genres = genres,
|
||||
posterShape = PosterShape.Poster,
|
||||
savedAtEpochMs = savedAtEpochMs,
|
||||
)
|
||||
|
||||
fun LibraryItem.toMetaPreview(): MetaPreview =
|
||||
MetaPreview(
|
||||
id = id,
|
||||
type = type,
|
||||
name = name,
|
||||
poster = poster,
|
||||
banner = banner,
|
||||
logo = logo,
|
||||
posterShape = posterShape,
|
||||
description = description,
|
||||
releaseInfo = releaseInfo,
|
||||
imdbRating = imdbRating,
|
||||
genres = genres,
|
||||
)
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
private data class StoredLibraryPayload(
|
||||
val items: List<LibraryItem> = emptyList(),
|
||||
)
|
||||
|
||||
object LibraryRepository {
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow(LibraryUiState())
|
||||
val uiState: StateFlow<LibraryUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var hasLoaded = false
|
||||
private var itemsById: MutableMap<String, LibraryItem> = mutableMapOf()
|
||||
|
||||
fun ensureLoaded() {
|
||||
if (hasLoaded) return
|
||||
hasLoaded = true
|
||||
|
||||
val payload = LibraryStorage.loadPayload().orEmpty().trim()
|
||||
if (payload.isNotEmpty()) {
|
||||
val items = runCatching {
|
||||
json.decodeFromString<StoredLibraryPayload>(payload).items
|
||||
}.getOrDefault(emptyList())
|
||||
itemsById = items.associateBy { it.id }.toMutableMap()
|
||||
}
|
||||
|
||||
publish()
|
||||
}
|
||||
|
||||
fun toggleSaved(item: LibraryItem) {
|
||||
ensureLoaded()
|
||||
if (itemsById.containsKey(item.id)) {
|
||||
remove(item.id)
|
||||
} else {
|
||||
save(item)
|
||||
}
|
||||
}
|
||||
|
||||
fun save(item: LibraryItem) {
|
||||
ensureLoaded()
|
||||
itemsById[item.id] = item.copy(savedAtEpochMs = LibraryClock.nowEpochMs())
|
||||
publish()
|
||||
persist()
|
||||
}
|
||||
|
||||
fun remove(id: String) {
|
||||
ensureLoaded()
|
||||
if (itemsById.remove(id) != null) {
|
||||
publish()
|
||||
persist()
|
||||
}
|
||||
}
|
||||
|
||||
fun isSaved(id: String): Boolean {
|
||||
ensureLoaded()
|
||||
return itemsById.containsKey(id)
|
||||
}
|
||||
|
||||
fun savedItem(id: String): LibraryItem? {
|
||||
ensureLoaded()
|
||||
return itemsById[id]
|
||||
}
|
||||
|
||||
private fun publish() {
|
||||
val items = itemsById.values
|
||||
.sortedByDescending { it.savedAtEpochMs }
|
||||
val sections = items
|
||||
.groupBy { it.type }
|
||||
.map { (type, typeItems) ->
|
||||
LibrarySection(
|
||||
type = type,
|
||||
displayTitle = type.toLibraryDisplayTitle(),
|
||||
items = typeItems.sortedByDescending { it.savedAtEpochMs },
|
||||
)
|
||||
}
|
||||
.sortedBy { it.displayTitle }
|
||||
|
||||
_uiState.value = LibraryUiState(
|
||||
items = items,
|
||||
sections = sections,
|
||||
isLoaded = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun persist() {
|
||||
LibraryStorage.savePayload(
|
||||
json.encodeToString(
|
||||
StoredLibraryPayload(
|
||||
items = itemsById.values.sortedByDescending { it.savedAtEpochMs },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun String.toLibraryDisplayTitle(): String {
|
||||
val normalized = trim()
|
||||
if (normalized.isBlank()) return "Other"
|
||||
|
||||
return normalized
|
||||
.split('-', '_', ' ')
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(" ") { token ->
|
||||
token.lowercase().replaceFirstChar { char -> char.uppercase() }
|
||||
}
|
||||
.ifBlank { "Other" }
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.nuvio.app.core.ui.NuvioScreen
|
||||
import com.nuvio.app.core.ui.NuvioScreenHeader
|
||||
import com.nuvio.app.core.ui.NuvioStatusModal
|
||||
import com.nuvio.app.core.ui.NuvioViewAllPillSize
|
||||
import com.nuvio.app.core.ui.NuvioShelfSection
|
||||
import com.nuvio.app.features.home.components.HomeEmptyStateCard
|
||||
import com.nuvio.app.features.home.components.HomePosterCard
|
||||
|
||||
@Composable
|
||||
fun LibraryScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onPosterClick: ((LibraryItem) -> Unit)? = null,
|
||||
) {
|
||||
val uiState by remember {
|
||||
LibraryRepository.ensureLoaded()
|
||||
LibraryRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
var pendingRemovalItem by remember { mutableStateOf<LibraryItem?>(null) }
|
||||
|
||||
NuvioScreen(
|
||||
modifier = modifier,
|
||||
horizontalPadding = 0.dp,
|
||||
) {
|
||||
stickyHeader {
|
||||
NuvioScreenHeader(
|
||||
title = "Library",
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
when {
|
||||
!uiState.isLoaded -> {
|
||||
item {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
uiState.sections.isEmpty() -> {
|
||||
item {
|
||||
HomeEmptyStateCard(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
title = "Your library is empty",
|
||||
message = "Saved titles will appear here after you tap Save on a details screen.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
librarySections(
|
||||
sections = uiState.sections,
|
||||
onPosterClick = onPosterClick,
|
||||
onPosterLongClick = { item -> pendingRemovalItem = item },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NuvioStatusModal(
|
||||
title = "Remove from Library?",
|
||||
message = pendingRemovalItem?.let { "Remove ${it.name} from your library?" }.orEmpty(),
|
||||
isVisible = pendingRemovalItem != null,
|
||||
confirmText = "Remove",
|
||||
dismissText = "Cancel",
|
||||
onConfirm = {
|
||||
pendingRemovalItem?.id?.let(LibraryRepository::remove)
|
||||
pendingRemovalItem = null
|
||||
},
|
||||
onDismiss = { pendingRemovalItem = null },
|
||||
)
|
||||
}
|
||||
|
||||
private fun LazyListScope.librarySections(
|
||||
sections: List<LibrarySection>,
|
||||
onPosterClick: ((LibraryItem) -> Unit)?,
|
||||
onPosterLongClick: (LibraryItem) -> Unit,
|
||||
) {
|
||||
items(
|
||||
items = sections,
|
||||
key = { section -> section.type },
|
||||
) { section ->
|
||||
NuvioShelfSection(
|
||||
title = section.displayTitle,
|
||||
entries = section.items,
|
||||
headerHorizontalPadding = 16.dp,
|
||||
rowContentPadding = PaddingValues(horizontal = 16.dp),
|
||||
viewAllPillSize = NuvioViewAllPillSize.Compact,
|
||||
key = { item -> "${item.type}:${item.id}" },
|
||||
) { item ->
|
||||
HomePosterCard(
|
||||
item = item.toMetaPreview(),
|
||||
onClick = onPosterClick?.let { { it(item) } },
|
||||
onLongClick = { onPosterLongClick(item) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
internal expect object LibraryStorage {
|
||||
fun loadPayload(): String?
|
||||
fun savePayload(payload: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
import com.nuvio.app.features.home.PosterShape
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class LibraryRepositoryTest {
|
||||
|
||||
@Test
|
||||
fun `display title uses exact type formatting`() {
|
||||
assertEquals("Movie", "movie".toLibraryDisplayTitle())
|
||||
assertEquals("Anime Series", "anime-series".toLibraryDisplayTitle())
|
||||
assertEquals("Tv", "tv".toLibraryDisplayTitle())
|
||||
assertEquals("Other", "".toLibraryDisplayTitle())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `meta preview mapping preserves exact type and poster shape`() {
|
||||
val item = LibraryItem(
|
||||
id = "tt1",
|
||||
type = "anime-series",
|
||||
name = "Title",
|
||||
poster = "poster",
|
||||
banner = "banner",
|
||||
logo = "logo",
|
||||
description = "desc",
|
||||
releaseInfo = "2024",
|
||||
imdbRating = "8.4",
|
||||
genres = listOf("Drama"),
|
||||
posterShape = PosterShape.Poster,
|
||||
savedAtEpochMs = 1L,
|
||||
)
|
||||
|
||||
val preview = item.toMetaPreview()
|
||||
|
||||
assertEquals("anime-series", preview.type)
|
||||
assertEquals(PosterShape.Poster, preview.posterShape)
|
||||
assertEquals("banner", preview.banner)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import platform.posix.time
|
||||
|
||||
actual object LibraryClock {
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun nowEpochMs(): Long = time(null) * 1000L
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
import platform.Foundation.NSUserDefaults
|
||||
|
||||
actual object LibraryStorage {
|
||||
private const val payloadKey = "library_payload"
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
NSUserDefaults.standardUserDefaults.stringForKey(payloadKey)
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
NSUserDefaults.standardUserDefaults.setObject(payload, forKey = payloadKey)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue