Move calendar business logic to fluxa-core via coreInvoke

Season-candidate selection, widget row formatting, and release
notification filtering now call the equivalent Rust calendar_plan
functions through core_invoke instead of duplicating that logic in
Kotlin.
This commit is contained in:
KhooLy 2026-07-18 15:01:13 +03:00
parent 0fa3ef723e
commit 276c4fb766
3 changed files with 96 additions and 37 deletions

View file

@ -12,7 +12,10 @@ import android.widget.RemoteViews
import com.fluxa.app.R
import com.fluxa.app.common.AppStrings
import com.fluxa.app.common.locale
import com.fluxa.app.core.rust.FluxaCoreUniFfi
import com.fluxa.app.ui.MainActivity
import com.google.gson.JsonArray as GsonArray
import com.google.gson.JsonObject as GsonObject
import org.json.JSONArray
import org.json.JSONObject
import java.text.SimpleDateFormat
@ -67,12 +70,12 @@ class CalendarWidgetProvider : AppWidgetProvider() {
accentColorArgb: Int = 0xFFFFFFFF.toInt()
) {
val rows = JSONArray().apply {
items.take(MAX_ROWS).forEach { item ->
fetchWidgetRows(items).forEach { row ->
put(JSONObject().apply {
put("date", formatWidgetDate(item.dateIso, language))
put("title", item.title)
put("subtitle", item.episodeTitle ?: item.subtitle.orEmpty())
put("episode", widgetEpisodeText(item))
put("date", formatWidgetDate(row.get("dateIso").asString, language))
put("title", row.get("title").asString)
put("subtitle", row.get("subtitle")?.asString.orEmpty())
put("episode", row.get("episodeText")?.asString.orEmpty())
})
}
}
@ -160,10 +163,25 @@ class CalendarWidgetProvider : AppWidgetProvider() {
}
}
private fun widgetEpisodeText(item: CalendarUpcomingItem): String {
val season = item.seasonNumber
val episode = item.episodeNumber
return if (season != null && episode != null) "S$season:E$episode" else ""
private fun fetchWidgetRows(items: List<CalendarUpcomingItem>): List<com.google.gson.JsonObject> {
val requestItems = GsonArray().apply {
items.forEach { item ->
add(GsonObject().apply {
addProperty("dateIso", item.dateIso)
addProperty("title", item.title)
item.subtitle?.let { addProperty("subtitle", it) }
item.episodeTitle?.let { addProperty("episodeTitle", it) }
item.seasonNumber?.let { addProperty("seasonNumber", it) }
item.episodeNumber?.let { addProperty("episodeNumber", it) }
})
}
}
val request = GsonObject().apply {
add("items", requestItems)
addProperty("maxRows", MAX_ROWS)
}
val value = FluxaCoreUniFfi.coreInvokeValue("calendarWidgetRows", request.toString())
return value.asJsonArray.map { it.asJsonObject }
}
private fun formatWidgetDate(dateIso: String, language: String): String {

View file

@ -2,6 +2,7 @@ package com.fluxa.app.ui.catalog
import com.fluxa.app.common.AppStrings
import com.fluxa.app.common.ReleaseDateUtils
import com.fluxa.app.core.rust.FluxaCoreUniFfi
import com.fluxa.app.data.local.*
import com.fluxa.app.data.local.UserProfile
import com.fluxa.app.data.local.WatchlistManager
@ -9,6 +10,9 @@ import com.fluxa.app.data.remote.Meta
import com.fluxa.app.data.remote.MetaDetail
import com.fluxa.app.data.remote.Video
import com.fluxa.app.data.repository.StremioRepository
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -27,6 +31,8 @@ internal class EpisodeCalendarLoader(
private val repository: StremioRepository,
private val watchlistManager: WatchlistManager
) {
private val gson = Gson()
suspend fun loadMonth(
profile: UserProfile?,
year: Int,
@ -154,18 +160,12 @@ internal class EpisodeCalendarLoader(
}
private fun calendarSeasonCandidates(meta: Meta, detail: MetaDetail): List<Int> {
val seasonsCount = detail.seasonsCount ?: meta.seasonsCount ?: 1
val watchedSeason = meta.lastVideoId
?.split(":")
?.getOrNull(1)
?.toIntOrNull()
val focused = listOfNotNull(
watchedSeason,
watchedSeason?.plus(1),
seasonsCount
).filter { it > 0 && it <= seasonsCount }
val full = if (seasonsCount <= 8) (1..seasonsCount).toList() else focused
return (focused + full).distinct().take(12)
val request = JsonObject().apply {
addProperty("seasonsCount", detail.seasonsCount ?: meta.seasonsCount ?: 1)
meta.lastVideoId?.let { addProperty("lastVideoId", it) }
}
val value = FluxaCoreUniFfi.coreInvokeValue("calendarSeasonCandidates", request.toString())
return gson.fromJson(value, object : TypeToken<List<Int>>() {}.type)
}
private fun UserProfile.hasExternalContinueProvider(): Boolean {

View file

@ -1,6 +1,7 @@
package com.fluxa.app.ui.catalog
import com.fluxa.app.common.AppStrings
import com.fluxa.app.core.rust.FluxaCoreUniFfi
import com.fluxa.app.data.local.*
import com.fluxa.app.data.remote.*
import com.fluxa.app.data.repository.*
@ -20,6 +21,8 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.fluxa.app.R
import com.fluxa.app.data.remote.StremioService
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Request
@ -30,15 +33,16 @@ object EpisodeNotificationHelper {
private const val NOTIFIED_KEYS = "notified_keys"
suspend fun notifyReleasedEpisodes(context: Context, profile: UserProfile?, items: List<CalendarUpcomingItem>, todayIso: String) {
if (profile?.safeNotificationsEnabled == false || profile?.safeAlertNewEpisodes == false) return
val releasedToday = items.filter { it.dateIso == todayIso && it.meta.type == "series" }
if (releasedToday.isEmpty()) return
val appContext = context.applicationContext
val prefs = appContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
val alreadyNotified = prefs.getStringSet(NOTIFIED_KEYS, emptySet()).orEmpty()
val content = fetchNotificationContent(items, todayIso, alreadyNotified, profile)
val releasedItems = content.getAsJsonArray("items")
if (releasedItems.isEmpty) return
if (!canPostNotifications(appContext)) return
ensureChannel(appContext)
val prefs = appContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
val notified = prefs.getStringSet(NOTIFIED_KEYS, emptySet()).orEmpty().toMutableSet()
val launchIntent = appContext.packageManager.getLaunchIntentForPackage(appContext.packageName)
val pendingIntent = launchIntent?.let {
PendingIntent.getActivity(
@ -49,15 +53,18 @@ object EpisodeNotificationHelper {
)
}
releasedToday.forEach { item ->
val key = "${profile?.id.orEmpty()}:${item.dateIso}:${item.meta.id}:${item.subtitle.orEmpty()}"
if (!notified.add(key)) return@forEach
val image = loadBitmap(item.artworkUrl())
val notificationTitle = AppStrings.t(
profile?.safeLanguage,
if (item.episodeNumber == 1) "notification.new_season_released" else "notification.new_episode_released"
)
val text = releaseNotificationText(item, profile?.safeLanguage)
releasedItems.forEach { element ->
val row = element.asJsonObject
val key = row.get("key").asString
val original = items.firstOrNull {
it.meta.id == row.get("metaId").asString &&
it.dateIso == row.get("dateIso").asString &&
it.seasonNumber == row.get("seasonNumber")?.takeIf { v -> !v.isJsonNull }?.asInt &&
it.episodeNumber == row.get("episodeNumber")?.takeIf { v -> !v.isJsonNull }?.asInt
} ?: return@forEach
val image = loadBitmap(original.artworkUrl())
val notificationTitle = AppStrings.t(profile?.safeLanguage, row.get("titleKey").asString)
val text = releaseNotificationText(original, profile?.safeLanguage)
val notification = NotificationCompat.Builder(appContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle(notificationTitle)
@ -79,7 +86,41 @@ object EpisodeNotificationHelper {
.build()
postNotification(appContext, key.hashCode(), notification)
}
prefs.edit().putStringSet(NOTIFIED_KEYS, notified).apply()
val newKeys = content.getAsJsonArray("keys").map { it.asString }
prefs.edit().putStringSet(NOTIFIED_KEYS, alreadyNotified + newKeys).apply()
}
private fun fetchNotificationContent(
items: List<CalendarUpcomingItem>,
todayIso: String,
alreadyNotified: Set<String>,
profile: UserProfile?
): JsonObject {
val requestItems = JsonArray().apply {
items.forEach { item ->
add(JsonObject().apply {
addProperty("dateIso", item.dateIso)
addProperty("metaId", item.meta.id)
addProperty("metaType", item.meta.type)
addProperty("title", item.title)
item.subtitle?.let { addProperty("subtitle", it) }
item.seasonNumber?.let { addProperty("seasonNumber", it) }
item.episodeNumber?.let { addProperty("episodeNumber", it) }
item.episodeTitle?.let { addProperty("episodeTitle", it) }
item.artworkUrl()?.let { addProperty("artworkUrl", it) }
})
}
}
val request = JsonObject().apply {
add("items", requestItems)
addProperty("todayIso", todayIso)
add("alreadyNotifiedKeys", JsonArray().apply { alreadyNotified.forEach { add(it) } })
profile?.id?.let { addProperty("profileId", it) }
addProperty("notificationsEnabled", profile?.safeNotificationsEnabled != false)
addProperty("alertNewEpisodes", profile?.safeAlertNewEpisodes != false)
}
return FluxaCoreUniFfi.coreInvokeValue("calendarNotificationContent", request.toString()).asJsonObject
}
private fun releaseNotificationText(item: CalendarUpcomingItem, language: String?): String {