mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-03 15:35:58 +00:00
Retire FluxaCoreNative's hand-written JNI in favor of core_invoke
Every stateless method on FluxaCoreNative now calls the corresponding core_invoke route via FluxaCoreUniFfi instead of a hand-written external fun; only the handle-lifecycle functions (headless engine, app core state) keep their dedicated JNI bindings. NuvioCoreBridge and ExternalLibraryClient's generic coreInvoke calls move onto the same UniFFI path. Public method signatures are unchanged, so no other call site needed to change.
This commit is contained in:
parent
c5df51f68a
commit
ae3e135b6f
10 changed files with 938 additions and 852 deletions
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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<Meta>,
|
||||
private val coreState: FluxaCoreStateHandle = FluxaCoreNative.createAppCoreState(
|
||||
private val coreState: FluxaUniFfiCoreStateHandle = FluxaCoreUniFfi.createAppCoreState(
|
||||
mapOf(
|
||||
"homeSearch" to mapOf(
|
||||
"searchHistory" to initialHistory
|
||||
|
|
|
|||
|
|
@ -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<List<AddonDescriptor>>() {}.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<HomeCategory>(),
|
||||
|
|
@ -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<Meta> = emptyList()
|
||||
private val _currentContinueWatchingCount = MutableStateFlow(0)
|
||||
val currentContinueWatchingCount: StateFlow<Int> = _currentContinueWatchingCount.asStateFlow()
|
||||
private val _syncingProviders = MutableStateFlow<Set<String>>(emptySet())
|
||||
val syncingProviders: StateFlow<Set<String>> = _syncingProviders.asStateFlow()
|
||||
|
||||
fun setProviderSyncing(provider: String, syncing: Boolean) {
|
||||
_syncingProviders.value = if (syncing) {
|
||||
_syncingProviders.value + provider
|
||||
} else {
|
||||
_syncingProviders.value - provider
|
||||
}
|
||||
}
|
||||
private var externalContinueWatching: List<Meta> = 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" })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<Int?>(null)
|
||||
var audioDelayMs by mutableLongStateOf(0L)
|
||||
var subtitleDelayMs by mutableLongStateOf(0L)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,12 @@
|
|||
package com.fluxa.app.data.repository
|
||||
|
||||
import android.util.Log
|
||||
import com.fluxa.app.core.rust.FluxaCoreUniFfi
|
||||
import com.fluxa.app.data.BuildConfig
|
||||
import com.fluxa.app.data.local.UserProfile
|
||||
import com.fluxa.app.data.local.safeLocalAddons
|
||||
import com.fluxa.app.data.local.safeLanguage
|
||||
import com.fluxa.app.data.remote.AnilistGraphQlRequest
|
||||
import com.fluxa.app.data.remote.Meta
|
||||
import com.fluxa.app.data.remote.MetaDetail
|
||||
import com.fluxa.app.data.remote.SimklEpisode
|
||||
|
|
@ -13,6 +15,7 @@ import com.fluxa.app.data.remote.TraktApi
|
|||
import com.fluxa.app.data.remote.TraktEpisode
|
||||
import com.fluxa.app.common.AppStrings
|
||||
import com.fluxa.app.domain.ContentIdentity
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
|
|
@ -25,6 +28,29 @@ import javax.inject.Singleton
|
|||
|
||||
private const val EPISODE_PROGRESS_UNIT_MS = 45 * 60_000L
|
||||
|
||||
private const val ANILIST_CURRENT_LIST_QUERY = """
|
||||
query (${'$'}userId: Int) {
|
||||
MediaListCollection(userId: ${'$'}userId, type: ANIME, status: CURRENT) {
|
||||
lists {
|
||||
entries {
|
||||
status
|
||||
progress
|
||||
updatedAt
|
||||
media {
|
||||
id
|
||||
title { english romaji native }
|
||||
coverImage { extraLarge large }
|
||||
bannerImage
|
||||
episodes
|
||||
seasonYear
|
||||
genres
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@Singleton
|
||||
class ExternalLibraryClient @Inject constructor(
|
||||
private val traktApi: TraktApi,
|
||||
|
|
@ -43,7 +69,8 @@ class ExternalLibraryClient @Inject constructor(
|
|||
val trakt = async { getTraktPlaybackItems(profile, language) }
|
||||
val mal = async { getMalContinueWatchingItems(profile.malAccessToken) }
|
||||
val simkl = async { getSimklContinueWatchingItems(profile.simklAccessToken) }
|
||||
val combined = trakt.await() + mal.await() + simkl.await()
|
||||
val anilist = async { getAnilistContinueWatchingItems(profile.anilistAccessToken) }
|
||||
val combined = trakt.await() + mal.await() + simkl.await() + anilist.await()
|
||||
distinctByIdentityKey(combined)
|
||||
}
|
||||
}
|
||||
|
|
@ -258,6 +285,75 @@ class ExternalLibraryClient @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun getAnilistContinueWatchingItems(token: String?): List<Meta> {
|
||||
if (token.isNullOrBlank()) return emptyList()
|
||||
val authorization = "Bearer $token"
|
||||
return try {
|
||||
val viewerResponse = traktApi.anilistGraphQl(
|
||||
authorization,
|
||||
AnilistGraphQlRequest(query = "query { Viewer { id } }")
|
||||
)
|
||||
val viewerId = viewerResponse
|
||||
.getAsJsonObject("data")
|
||||
?.getAsJsonObject("Viewer")
|
||||
?.get("id")
|
||||
?.takeUnless { it.isJsonNull }
|
||||
?.asInt ?: return emptyList()
|
||||
|
||||
val listResponse = traktApi.anilistGraphQl(
|
||||
authorization,
|
||||
AnilistGraphQlRequest(
|
||||
query = ANILIST_CURRENT_LIST_QUERY,
|
||||
variables = mapOf("userId" to viewerId)
|
||||
)
|
||||
)
|
||||
val lists = listResponse
|
||||
.getAsJsonObject("data")
|
||||
?.getAsJsonObject("MediaListCollection")
|
||||
?.getAsJsonArray("lists")
|
||||
?: com.google.gson.JsonArray()
|
||||
val entries = com.google.gson.JsonArray()
|
||||
lists.forEach { list ->
|
||||
list.asJsonObject.getAsJsonArray("entries")?.forEach(entries::add)
|
||||
}
|
||||
if (entries.size() == 0) return emptyList()
|
||||
|
||||
val args = com.google.gson.JsonObject().apply {
|
||||
add("entries", entries)
|
||||
addProperty("nowMs", System.currentTimeMillis())
|
||||
}
|
||||
val envelope = JsonParser.parseString(
|
||||
FluxaCoreUniFfi.coreInvoke("anilistEntriesToSync", args.toString())
|
||||
).asJsonObject
|
||||
if (envelope.get("ok")?.asBoolean != true) return emptyList()
|
||||
val progress = envelope.getAsJsonObject("value")?.getAsJsonObject("progress") ?: return emptyList()
|
||||
progress.entrySet().mapNotNull { (_, value) -> anilistProgressEntryToMeta(value.asJsonObject) }
|
||||
} catch (e: Exception) {
|
||||
Log.w("ExternalLibraryClient", "Failed to load AniList continue watching items", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun anilistProgressEntryToMeta(entry: com.google.gson.JsonObject): Meta? {
|
||||
val meta = entry.getAsJsonObject("meta") ?: return null
|
||||
val id = meta.get("id")?.takeUnless { it.isJsonNull }?.asString ?: return null
|
||||
val savedAt = entry.get("savedAt")?.takeUnless { it.isJsonNull }?.asString
|
||||
val lastWatchedAt = savedAt?.let { runCatching { java.time.Instant.parse(it).toEpochMilli() }.getOrNull() }
|
||||
return Meta(
|
||||
id = id,
|
||||
name = meta.get("name")?.takeUnless { it.isJsonNull }?.asString ?: id,
|
||||
type = meta.get("type")?.takeUnless { it.isJsonNull }?.asString ?: "series",
|
||||
poster = meta.get("poster")?.takeUnless { it.isJsonNull }?.asString,
|
||||
background = meta.get("background")?.takeUnless { it.isJsonNull }?.asString,
|
||||
timeOffset = entry.get("timeOffset")?.takeUnless { it.isJsonNull }?.asLong,
|
||||
duration = entry.get("duration")?.takeUnless { it.isJsonNull }?.asLong,
|
||||
lastVideoId = entry.get("lastVideoId")?.takeUnless { it.isJsonNull }?.asString,
|
||||
lastEpisodeName = entry.get("lastEpisodeName")?.takeUnless { it.isJsonNull }?.asString,
|
||||
lastWatchedAt = lastWatchedAt,
|
||||
reason = "AniList"
|
||||
)
|
||||
}
|
||||
|
||||
private fun findEpisodeArtwork(detail: MetaDetail?, lastVideoId: String?, episode: TraktEpisode?): String? {
|
||||
val videos = detail?.videos.orEmpty()
|
||||
if (videos.isEmpty()) return null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package com.fluxa.app.data.repository
|
||||
|
||||
import com.fluxa.app.core.rust.FluxaCoreUniFfi
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
|
||||
object NuvioCoreBridge {
|
||||
fun buildLocalProfiles(
|
||||
sessionProfile: JsonObject,
|
||||
nuvioProfiles: JsonElement,
|
||||
avatarCatalog: JsonElement,
|
||||
existingProfiles: JsonElement
|
||||
): JsonArray = invoke(
|
||||
"nuvioBuildLocalProfiles",
|
||||
JsonObject().apply {
|
||||
add("sessionProfile", sessionProfile)
|
||||
add("nuvioProfiles", nuvioProfiles)
|
||||
add("avatarCatalog", avatarCatalog)
|
||||
add("existingProfiles", existingProfiles)
|
||||
}
|
||||
).asJsonArray
|
||||
|
||||
fun libraryToWatchlist(library: JsonElement): JsonArray = invoke(
|
||||
"nuvioLibraryToWatchlist",
|
||||
JsonObject().apply { add("library", library) }
|
||||
).asJsonArray
|
||||
|
||||
fun progressMetaNeeds(watchProgress: JsonElement, library: JsonElement): JsonArray = invoke(
|
||||
"nuvioProgressMetaNeeds",
|
||||
JsonObject().apply {
|
||||
add("watchProgress", watchProgress)
|
||||
add("library", library)
|
||||
}
|
||||
).asJsonArray
|
||||
|
||||
fun importMergePlan(
|
||||
library: JsonElement,
|
||||
addonMetas: JsonObject,
|
||||
watchProgress: JsonElement,
|
||||
watchHistory: JsonElement
|
||||
): JsonObject = invoke(
|
||||
"nuvioImportMergePlan",
|
||||
JsonObject().apply {
|
||||
add("progress", JsonObject())
|
||||
add("watched", JsonObject())
|
||||
add("library", library)
|
||||
add("addonMetas", addonMetas)
|
||||
add("watchProgress", watchProgress)
|
||||
add("watchHistory", watchHistory)
|
||||
}
|
||||
).asJsonObject
|
||||
|
||||
fun mapCollections(collections: JsonElement): JsonArray = invoke(
|
||||
"nuvioMapCollections",
|
||||
JsonObject().apply { add("collections", collections) }
|
||||
).asJsonArray
|
||||
|
||||
private fun invoke(method: String, args: JsonObject): JsonElement =
|
||||
FluxaCoreUniFfi.coreInvokeValue(method, args.toString())
|
||||
}
|
||||
Loading…
Reference in a new issue