diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeLibraryCoordinator.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeLibraryCoordinator.kt index 83c5c5d..6fe19ab 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeLibraryCoordinator.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeLibraryCoordinator.kt @@ -1,7 +1,7 @@ package com.fluxa.app.ui.catalog import android.util.Log -import com.fluxa.app.core.rust.FluxaCoreStateHandle +import com.fluxa.app.core.rust.FluxaUniFfiCoreStateHandle import com.fluxa.app.data.local.* import com.fluxa.app.data.local.UserProfile import com.fluxa.app.data.remote.Meta @@ -36,7 +36,7 @@ internal class HomeLibraryCoordinator( private val repository: StremioRepository, private val traktRepository: TraktRepository, private val scope: CoroutineScope, - private val coreState: FluxaCoreStateHandle, + private val coreState: FluxaUniFfiCoreStateHandle, private val gson: Gson ) { private val _state = MutableStateFlow(LibraryUiState()) diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeSearchFocusStateHolder.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeSearchFocusStateHolder.kt index d23fa83..7f598a9 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeSearchFocusStateHolder.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeSearchFocusStateHolder.kt @@ -1,7 +1,7 @@ package com.fluxa.app.ui.catalog -import com.fluxa.app.core.rust.FluxaCoreNative -import com.fluxa.app.core.rust.FluxaCoreStateHandle +import com.fluxa.app.core.rust.FluxaCoreUniFfi +import com.fluxa.app.core.rust.FluxaUniFfiCoreStateHandle import com.fluxa.app.data.remote.Meta import com.google.gson.Gson import java.io.Closeable @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.asStateFlow class HomeSearchFocusStateHolder( initialHistory: List, - private val coreState: FluxaCoreStateHandle = FluxaCoreNative.createAppCoreState( + private val coreState: FluxaUniFfiCoreStateHandle = FluxaCoreUniFfi.createAppCoreState( mapOf( "homeSearch" to mapOf( "searchHistory" to initialHistory diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt index 10cc57c..85ee1cc 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt @@ -6,7 +6,8 @@ import com.fluxa.app.data.remote.* import com.fluxa.app.data.repository.* import com.fluxa.app.core.rust.FluxaAndroidHeadlessEnvironment import com.fluxa.app.core.rust.FluxaCoreNative -import com.fluxa.app.core.rust.FluxaCoreStateHandle +import com.fluxa.app.core.rust.FluxaCoreUniFfi +import com.fluxa.app.core.rust.FluxaUniFfiCoreStateHandle import com.fluxa.app.core.rust.FluxaHeadlessRuntimeFactory import com.fluxa.app.domain.discovery.DiscoverCatalogOption import com.fluxa.app.domain.discovery.MetadataFeedOption @@ -71,7 +72,7 @@ class HomeViewModel @Inject constructor( private val addonListType = object : TypeToken>() {}.type private val headlessRuntime = FluxaHeadlessRuntimeFactory.createUniFfi(headlessEnvironment) private val initialSearchHistory = searchHistoryStore.load(null) - private val coreState: FluxaCoreStateHandle = FluxaCoreNative.createAppCoreState( + private val coreState: FluxaUniFfiCoreStateHandle = FluxaCoreUniFfi.createAppCoreState( mapOf( "home" to mapOf( "categories" to emptyList(), @@ -143,6 +144,7 @@ class HomeViewModel @Inject constructor( scope = viewModelScope, gson = gson, dispatch = ::dispatchHeadless, + activeProfile = { currentActiveProfile }, setActiveProfile = { setActiveProfileState(it) }, setWatchlist = ::setWatchlistState, setContinueWatching = ::setCurrentWatchlistState, @@ -237,6 +239,16 @@ class HomeViewModel @Inject constructor( private var currentWatchlist: List = emptyList() private val _currentContinueWatchingCount = MutableStateFlow(0) val currentContinueWatchingCount: StateFlow = _currentContinueWatchingCount.asStateFlow() + private val _syncingProviders = MutableStateFlow>(emptySet()) + val syncingProviders: StateFlow> = _syncingProviders.asStateFlow() + + fun setProviderSyncing(provider: String, syncing: Boolean) { + _syncingProviders.value = if (syncing) { + _syncingProviders.value + provider + } else { + _syncingProviders.value - provider + } + } private var externalContinueWatching: List = emptyList() private var traktWatchedState: TraktWatchedState = TraktWatchedState() private var currentActiveProfile: UserProfile? = null @@ -1086,7 +1098,7 @@ class HomeViewModel @Inject constructor( cacheSaveJob?.cancel() cacheSaveJob = viewModelScope.launch(Dispatchers.Default) { delay(600) - homeCategoryCache.save(profile, categories) + homeCategoryCache.save(profile, categories.filterNot { it.id == "continue_watching" }) } } diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModelCoordinatorFactory.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModelCoordinatorFactory.kt index cd62bb1..3c086d8 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModelCoordinatorFactory.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModelCoordinatorFactory.kt @@ -1,7 +1,7 @@ package com.fluxa.app.ui.catalog import android.content.Context -import com.fluxa.app.core.rust.FluxaCoreStateHandle +import com.fluxa.app.core.rust.FluxaUniFfiCoreStateHandle import com.fluxa.app.data.local.UserProfile import com.fluxa.app.data.local.WatchlistManager import com.fluxa.app.data.remote.AddonDescriptor @@ -19,7 +19,7 @@ class HomeViewModelCoordinatorFactory @Inject constructor() { repository: StremioRepository, traktRepository: TraktRepository, scope: CoroutineScope, - coreState: FluxaCoreStateHandle, + coreState: FluxaUniFfiCoreStateHandle, gson: Gson ): HomeLibraryCoordinator { return HomeLibraryCoordinator(repository, traktRepository, scope, coreState, gson) diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenState.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenState.kt index cda62e4..93b500c 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenState.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenState.kt @@ -11,8 +11,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.media3.ui.AspectRatioFrameLayout -import com.fluxa.app.core.rust.FluxaCoreNative -import com.fluxa.app.core.rust.FluxaCoreStateHandle +import com.fluxa.app.core.rust.FluxaCoreUniFfi +import com.fluxa.app.core.rust.FluxaUniFfiCoreStateHandle import com.fluxa.app.data.remote.IntroTimestamps import com.fluxa.app.data.remote.Stream import com.fluxa.app.data.remote.Video @@ -39,7 +39,7 @@ internal class PlayerScreenState( initialVolume: Int ) { private val gson = Gson() - private val coreState: FluxaCoreStateHandle = FluxaCoreNative.createAppCoreState( + private val coreState: FluxaUniFfiCoreStateHandle = FluxaCoreUniFfi.createAppCoreState( mapOf( "player" to mapOf( "currentVideoId" to initialVideoId, @@ -111,7 +111,6 @@ internal class PlayerScreenState( var showSettings by mutableStateOf(false) var activeSettingsTab by mutableIntStateOf(0) - var inlineDelayTarget by mutableStateOf(null) var audioDelayMs by mutableLongStateOf(0L) var subtitleDelayMs by mutableLongStateOf(0L) diff --git a/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreBenchmarkTest.kt b/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreBenchmarkTest.kt index ca8a7e4..eea1a13 100644 --- a/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreBenchmarkTest.kt +++ b/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreBenchmarkTest.kt @@ -407,11 +407,11 @@ class FluxaCoreBenchmarkTest { !name.contains("cacheTrimPolicy", ignoreCase = true) } assertTrue(nativeCacheStorageFunctions.isEmpty()) - assertTrue(nativeFunctionNames.any { it == "cacheEntryPolicyJsonNative" }) - assertTrue(nativeFunctionNames.any { it == "cacheTrimPolicyJsonNative" }) + assertTrue(nativeFunctionNames.any { it == "cacheEntryPolicy" }) + assertTrue(nativeFunctionNames.any { it == "cacheTrimPolicy" }) assertFalse(nativeFunctionNames.any { it == "startLocalStreamServerNative" }) assertFalse(nativeFunctionNames.any { it == "stopLocalStreamServerNative" }) - assertTrue(nativeFunctionNames.any { it == "parseManifestJsonNative" }) + assertTrue(nativeFunctionNames.any { it == "coreInvokeNative" }) } private fun startStaticServer(payload: ByteArray, requestCount: AtomicInteger): StaticByteServer { diff --git a/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreUniFfiContractTest.kt b/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreUniFfiContractTest.kt index 9f54fcc..4839020 100644 --- a/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreUniFfiContractTest.kt +++ b/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreUniFfiContractTest.kt @@ -34,4 +34,13 @@ class FluxaCoreUniFfiContractTest { assertEquals("Movie", meta["name"]) } } + + @Test + fun coreInvokeRoutesCalendarMethodThroughUniFfi() { + val value = FluxaCoreUniFfi.coreInvokeValue( + "calendarSeasonCandidates", + """{"seasonsCount":10,"lastVideoId":"tt1:2:3"}""" + ) + assertEquals("[2,3,10]", value.toString()) + } } diff --git a/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreNative.kt b/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreNative.kt index 0dd156d..90758de 100644 --- a/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreNative.kt +++ b/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreNative.kt @@ -69,6 +69,8 @@ import com.google.gson.stream.JsonReader import java.io.Closeable import java.io.StringReader +private fun urlArgs(url: String): String = JsonObject().apply { addProperty("url", url) }.toString() + private fun JsonObject.stringOrNull(key: String): String? = get(key)?.takeUnless { it.isJsonNull }?.asString @@ -211,13 +213,6 @@ data class NativeCloudstreamRequest( val timeoutMs: Long = 0L ) -// ── calendar_plan ───────────────────────────────────────────────────────────── - -data class NativeCalendarNotificationContent( - val items: List> = emptyList(), - val keys: List = emptyList() -) - data class NativeDetailSeasonLoadPlan( val firstSeasonToLoad: Int = 1, val savedSeason: Int? = null @@ -599,10 +594,10 @@ object FluxaCoreNative { }.getOrDefault(emptyMap()) } - fun coreCapabilities(portable: Boolean = false): NativeCoreCapabilitySet = call { - val json = coreCapabilitiesJsonNative(portable) - json.takeIf { it.isNotBlank() }?.let { gson.fromJson(it, NativeCoreCapabilitySet::class.java) } - ?: NativeCoreCapabilitySet() + fun coreCapabilities(portable: Boolean = false): NativeCoreCapabilitySet { + val args = JsonObject().apply { addProperty("portable", portable) } + val value = FluxaCoreUniFfi.coreInvokeValue("coreCapabilities", args.toString()) + return gson.fromJson(value, NativeCoreCapabilitySet::class.java) ?: NativeCoreCapabilitySet() } internal fun appCoreStateJson(handle: Long): String = call { @@ -621,53 +616,60 @@ object FluxaCoreNative { coreInvokeNative(method, argsJson).orEmpty() } - fun normalizeManifestUrl(rawUrl: String): String = call { normalizeManifestUrlNative(rawUrl) } + fun normalizeManifestUrl(rawUrl: String): String = + FluxaCoreUniFfi.coreInvokeValue("normalizeManifestUrl", urlArgs(rawUrl)).asString - fun identity(rawUrl: String): String = call { identityNative(rawUrl) } + fun identity(rawUrl: String): String = + FluxaCoreUniFfi.coreInvokeValue("identity", urlArgs(rawUrl)).asString - fun manifestCandidates(rawUrl: String): List = call { - manifestCandidatesJsonNative(rawUrl) - }.let { json -> - gson.fromJson>(json, stringListType) + fun manifestCandidates(rawUrl: String): List { + val value = FluxaCoreUniFfi.coreInvokeValue("manifestCandidates", urlArgs(rawUrl)) + return gson.fromJson(value, stringListType) ?: emptyList() } - fun manifestFetchPlan(rawUrl: String): NativeManifestFetchPlan? = call { - manifestFetchPlanJsonNative(rawUrl) - ?.takeIf { it.isNotBlank() } - ?.let { gson.fromJson(it, NativeManifestFetchPlan::class.java) } + fun manifestFetchPlan(rawUrl: String): NativeManifestFetchPlan? { + val value = FluxaCoreUniFfi.coreInvokeValue("manifestFetchPlan", urlArgs(rawUrl)) + return value.takeUnless { it.isJsonNull }?.let { gson.fromJson(it, NativeManifestFetchPlan::class.java) } } - fun baseUrl(rawUrl: String): String = call { baseUrlNative(rawUrl) } + fun baseUrl(rawUrl: String): String = + FluxaCoreUniFfi.coreInvokeValue("baseUrl", urlArgs(rawUrl)).asString fun preferHttpsAssetUrl(rawUrl: String): String? { - val resolved = call { - preferHttpsAssetUrlNative(rawUrl).takeIf { it.isNotBlank() } - } - return resolved + val value = FluxaCoreUniFfi.coreInvokeValue("preferHttpsAssetUrl", urlArgs(rawUrl)) + return value.takeUnless { it.isJsonNull }?.asString?.takeIf { it.isNotBlank() } } - fun addonStoreInputType(text: String): String = call { - addonStoreInputTypeNative(text) - } + fun addonStoreInputType(text: String): String = + FluxaCoreUniFfi.coreInvokeValue( + "addonStoreInputType", + JsonObject().apply { addProperty("input", text) }.toString() + ).asString - fun normalizeCloudstreamRepoUrl(rawUrl: String): String = call { - normalizeCloudstreamRepoUrlNative(rawUrl) - } + fun normalizeCloudstreamRepoUrl(rawUrl: String): String = + FluxaCoreUniFfi.coreInvokeValue("normalizeCloudstreamRepoUrl", urlArgs(rawUrl)).asString - fun normalizePluginRepositoryUrl(rawUrl: String): String = call { - normalizePluginRepositoryUrlNative(rawUrl) - } + fun normalizePluginRepositoryUrl(rawUrl: String): String = + FluxaCoreUniFfi.coreInvokeValue("normalizePluginRepositoryUrl", urlArgs(rawUrl)).asString - fun pluginIsSecureRemoteUrl(url: String): Boolean = call { - pluginIsSecureRemoteUrlNative(url) - } + fun pluginIsSecureRemoteUrl(url: String): Boolean = + FluxaCoreUniFfi.coreInvokeValue("isSecureRemoteUrl", urlArgs(url)).asBoolean - fun pluginSameRepositoryUrl(left: String, right: String): Boolean = call { - pluginSameRepositoryUrlNative(left, right) - } + fun pluginSameRepositoryUrl(left: String, right: String): Boolean = + FluxaCoreUniFfi.coreInvokeValue( + "samePluginRepositoryUrl", + JsonObject().apply { + addProperty("left", left) + addProperty("right", right) + }.toString() + ).asBoolean - fun extractAddonManifestUrl(detailText: String): String? = call { - extractAddonManifestUrlNative(detailText).takeIf { it.isNotBlank() } + fun extractAddonManifestUrl(detailText: String): String? { + val value = FluxaCoreUniFfi.coreInvokeValue( + "extractAddonManifestUrl", + JsonObject().apply { addProperty("text", detailText) }.toString() + ) + return value.takeUnless { it.isJsonNull }?.asString?.takeIf { it.isNotBlank() } } fun addonStoreSearchPolicy( @@ -675,54 +677,52 @@ object FluxaCoreNative { nowMillis: Long, cachedAtMillis: Long?, ttlMillis: Long - ): NativeAddonStoreSearchPolicy = call { + ): NativeAddonStoreSearchPolicy { val request = NativeAddonStoreSearchPolicyRequest( query = query, nowMillis = nowMillis, cachedAtMillis = cachedAtMillis, ttlMillis = ttlMillis ) - gson.fromJson( - addonStoreSearchPolicyJsonNative(gson.toJson(request)), - NativeAddonStoreSearchPolicy::class.java - ) ?: NativeAddonStoreSearchPolicy() + val value = FluxaCoreUniFfi.coreInvokeValue("addonStoreSearchPolicy", gson.toJson(request)) + return gson.fromJson(value, NativeAddonStoreSearchPolicy::class.java) ?: NativeAddonStoreSearchPolicy() } fun repositoryMetaDetailPlan( useConfiguredAddons: Boolean, authKey: String?, localAddons: List? - ): NativeRepositoryMetaDetailPlan = call { + ): NativeRepositoryMetaDetailPlan { val request = NativeRepositoryMetaDetailPlanRequest( useConfiguredAddons = useConfiguredAddons, authKey = authKey.orEmpty(), localAddons = localAddons.orEmpty() ) - gson.fromJson( - repositoryMetaDetailPlanJsonNative(gson.toJson(request)), - NativeRepositoryMetaDetailPlan::class.java - ) ?: NativeRepositoryMetaDetailPlan() + val value = FluxaCoreUniFfi.coreInvokeValue("repositoryMetaDetailPlan", gson.toJson(request)) + return gson.fromJson(value, NativeRepositoryMetaDetailPlan::class.java) ?: NativeRepositoryMetaDetailPlan() } - fun repositorySeasonVideos(metaDetail: MetaDetail?, seasonNumber: Int): List