diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt index dd1455b64..41b9b5695 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt @@ -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) diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/library/LibraryClock.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/library/LibraryClock.android.kt new file mode 100644 index 000000000..bcfbf061c --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/library/LibraryClock.android.kt @@ -0,0 +1,5 @@ +package com.nuvio.app.features.library + +actual object LibraryClock { + actual fun nowEpochMs(): Long = System.currentTimeMillis() +} diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/library/LibraryStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/library/LibraryStorage.android.kt new file mode 100644 index 000000000..565b8585e --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/library/LibraryStorage.android.kt @@ -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() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 1e8df0fbc..23f51883d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -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, ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioShelfComponents.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioShelfComponents.kt index ec08d2bcf..6b67a4418 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioShelfComponents.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioShelfComponents.kt @@ -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, + ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt index 63987ef36..f07aa7a65 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt @@ -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) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailActionButtons.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailActionButtons.kt index 8c739847f..761849a02 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailActionButtons.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailActionButtons.kt @@ -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, ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt index c9febac6f..5f87ccd0e 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt @@ -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, ) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryClock.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryClock.kt new file mode 100644 index 000000000..b9251b1e8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryClock.kt @@ -0,0 +1,5 @@ +package com.nuvio.app.features.library + +internal expect object LibraryClock { + fun nowEpochMs(): Long +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryModels.kt new file mode 100644 index 000000000..9e5726eea --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryModels.kt @@ -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 = emptyList(), + val posterShape: PosterShape = PosterShape.Poster, + val savedAtEpochMs: Long, +) + +data class LibrarySection( + val type: String, + val displayTitle: String, + val items: List, +) + +data class LibraryUiState( + val items: List = emptyList(), + val sections: List = 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, + ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt new file mode 100644 index 000000000..b352a51b9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt @@ -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 = emptyList(), +) + +object LibraryRepository { + private val json = Json { + ignoreUnknownKeys = true + encodeDefaults = true + } + + private val _uiState = MutableStateFlow(LibraryUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var hasLoaded = false + private var itemsById: MutableMap = mutableMapOf() + + fun ensureLoaded() { + if (hasLoaded) return + hasLoaded = true + + val payload = LibraryStorage.loadPayload().orEmpty().trim() + if (payload.isNotEmpty()) { + val items = runCatching { + json.decodeFromString(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" } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt new file mode 100644 index 000000000..9f3b6aab1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt @@ -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(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, + 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) }, + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryStorage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryStorage.kt new file mode 100644 index 000000000..4b18bf758 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryStorage.kt @@ -0,0 +1,6 @@ +package com.nuvio.app.features.library + +internal expect object LibraryStorage { + fun loadPayload(): String? + fun savePayload(payload: String) +} diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/library/LibraryRepositoryTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/library/LibraryRepositoryTest.kt new file mode 100644 index 000000000..b33fe936f --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/library/LibraryRepositoryTest.kt @@ -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) + } +} diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/library/LibraryClock.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/library/LibraryClock.ios.kt new file mode 100644 index 000000000..3fb940569 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/library/LibraryClock.ios.kt @@ -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 +} diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/library/LibraryStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/library/LibraryStorage.ios.kt new file mode 100644 index 000000000..8e2e157f6 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/library/LibraryStorage.ios.kt @@ -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) + } +}