@@ -17,10 +17,13 @@ import app.morphe.manager.data.room.bundles.PatchBundleProperties
1717import app.morphe.manager.data.room.bundles.Source
1818import app.morphe.manager.domain.bundles.*
1919import app.morphe.manager.domain.manager.PreferencesManager
20- import app.morphe.manager.patcher.patch.PatchBundle
20+ import app.morphe.manager.network.utils.APIError
2121import app.morphe.manager.patcher.patch.BundleAppMetadata
22+ import app.morphe.manager.patcher.patch.PatchBundle
2223import app.morphe.manager.patcher.patch.PatchBundleInfo
24+ import app.morphe.manager.ui.viewmodel.BundleSnapshot
2325import app.morphe.manager.util.*
26+ import io.ktor.client.plugins.ResponseException
2427import io.ktor.http.Url
2528import kotlinx.collections.immutable.PersistentMap
2629import kotlinx.collections.immutable.mutate
@@ -40,10 +43,6 @@ import java.nio.ByteOrder
4043import java.nio.charset.StandardCharsets
4144import java.security.MessageDigest
4245import java.util.Locale
43- import app.morphe.manager.util.syncFcmTopics
44- import app.morphe.manager.network.utils.APIError
45- import app.morphe.manager.ui.viewmodel.BundleSnapshot
46- import io.ktor.client.plugins.ResponseException
4746import app.morphe.manager.data.room.bundles.Source as SourceInfo
4847
4948class PatchBundleRepository (
@@ -56,15 +55,18 @@ class PatchBundleRepository(
5655 private val bundlesDir = app.getDir(" patch_bundles" , Context .MODE_PRIVATE )
5756
5857 private val scope = CoroutineScope (Dispatchers .Default )
59- private val store = Store (scope, State () )
58+ private val store = Store < BundleState > (scope, BundleState . Loading )
6059
61- val sources = store.state.map { it.sources.values.toList() }
60+ val bundleState: StateFlow <BundleState > = store.state
61+ .stateIn(scope, SharingStarted .Eagerly , BundleState .Loading )
62+
63+ val sources = store.state.map { (it as ? BundleState .Ready )?.sources?.values?.toList() ? : emptyList() }
6264 val bundles = store.state.map {
63- it. sources.mapNotNull { (uid, src) ->
65+ (it as ? BundleState . Ready )?. sources? .mapNotNull { (uid, src) ->
6466 uid to (src.patchBundle ? : return @mapNotNull null )
65- }.toMap()
67+ }? .toMap() ? : emptyMap ()
6668 }
67- val allBundlesInfoFlow = store.state.map { it. info }
69+ val allBundlesInfoFlow = store.state.map { (it as ? BundleState . Ready )?. info ? : persistentMapOf() }
6870 val enabledBundlesInfoFlow = allBundlesInfoFlow.map { info ->
6971 info.filter { (_, bundleInfo) -> bundleInfo.enabled }
7072 }
@@ -80,9 +82,6 @@ class PatchBundleRepository(
8082 .map { BundleAppMetadata .buildFrom(it) }
8183 .stateIn(scope, SharingStarted .Eagerly , emptyMap())
8284
83- /* * True once [doReload] has completed at least once (even if all bundles are disabled). */
84- val isBundlePipelineLoaded: Flow <Boolean > = store.state.map { it.isLoaded }
85-
8685 fun scopedBundleInfoFlow (packageName : String , version : String? ) = enabledBundlesInfoFlow.map {
8786 it.map { (_, bundleInfo) ->
8887 bundleInfo.forPackage(
@@ -324,18 +323,18 @@ class PatchBundleRepository(
324323
325324 private suspend inline fun dispatchAction (
326325 name : String ,
327- crossinline block : suspend ActionContext .(current: State ) -> State
326+ crossinline block : suspend ActionContext .(current: BundleState ) -> BundleState
328327 ) {
329- store.dispatch(object : Action <State > {
330- override suspend fun ActionContext.execute (current : State ) = block(current)
328+ store.dispatch(object : Action <BundleState > {
329+ override suspend fun ActionContext.execute (current : BundleState ) = block(current)
331330 override fun toString () = name
332331 })
333332 }
334333
335334 /* *
336335 * Performs a reload. Do not call this outside of a store action.
337336 */
338- private suspend fun doReload (): State {
337+ private suspend fun doReload (): BundleState . Ready {
339338 val entities = loadEntitiesEnforcingOfficialOrder()
340339
341340 val sources = entities.associate { it.uid to it.load() }.toMutableMap()
@@ -344,22 +343,23 @@ class PatchBundleRepository(
344343 if (hasOutOfDateNames) dispatchAction(
345344 " Sync names"
346345 ) { state ->
347- val nameChanges = state.sources.mapNotNull { (_, src) ->
346+ val ready = state as ? BundleState .Ready ? : return @dispatchAction state
347+ val nameChanges = ready.sources.mapNotNull { (_, src) ->
348348 if (! src.isNameOutOfDate) return @mapNotNull null
349349 val newName = src.patchBundle?.manifestAttributes?.name?.takeIf { it != src.name }
350350 ? : return @mapNotNull null
351351
352352 src.uid to newName
353353 }
354- val sources = state .sources.toMutableMap()
355- val info = state .info.toMutableMap()
354+ val sources = ready .sources.toMutableMap()
355+ val info = ready .info.toMutableMap()
356356 nameChanges.forEach { (uid, name) ->
357357 updateDb(uid) { it.copy(name = name) }
358358 sources[uid] = sources[uid]!! .copy(name = name)
359359 info[uid] = info[uid]?.copy(name = name) ? : return @forEach
360360 }
361361
362- State (sources.toPersistentMap(), info.toPersistentMap())
362+ ready.copy (sources = sources .toPersistentMap(), info = info.toPersistentMap())
363363 }
364364 val info = loadMetadata(sources).toMutableMap()
365365
@@ -377,7 +377,7 @@ class PatchBundleRepository(
377377 }
378378 }
379379
380- return State (sources.toPersistentMap(), info.toPersistentMap(), isLoaded = true )
380+ return BundleState . Ready (sources.toPersistentMap(), info.toPersistentMap())
381381 }
382382
383383 suspend fun reload () = dispatchAction(" Full reload" ) {
@@ -424,7 +424,8 @@ class PatchBundleRepository(
424424
425425 if (failures.isNotEmpty()) {
426426 dispatchAction(" Mark bundles as failed" ) { state ->
427- state.copy(sources = state.sources.mutate {
427+ val ready = state as ? BundleState .Ready ? : return @dispatchAction state
428+ ready.copy(sources = ready.sources.mutate {
428429 failures.forEach { (uid, throwable) ->
429430 it[uid] = it[uid]?.copy(error = throwable) ? : return @forEach
430431 }
@@ -601,7 +602,7 @@ class PatchBundleRepository(
601602
602603 suspend fun reset () = dispatchAction(" Reset" ) { state ->
603604 dao.reset()
604- state. sources.keys.forEach { directoryOf(it).deleteRecursively() }
605+ ( state as ? BundleState . Ready )?. sources? .keys? .forEach { directoryOf(it).deleteRecursively() }
605606 doReload()
606607 }
607608
@@ -694,8 +695,9 @@ class PatchBundleRepository(
694695
695696 suspend fun remove (vararg bundles : PatchBundleSource ) =
696697 dispatchAction(" Remove (${bundles.map { it.uid }.joinToString(" ," )} )" ) { state ->
697- val sources = state.sources.toMutableMap()
698- val info = state.info.toMutableMap()
698+ val ready = state as ? BundleState .Ready ? : return @dispatchAction state
699+ val sources = ready.sources.toMutableMap()
700+ val info = ready.info.toMutableMap()
699701 bundles.forEach {
700702 dao.remove(it.uid)
701703 directoryOf(it.uid).deleteRecursively()
@@ -706,7 +708,7 @@ class PatchBundleRepository(
706708 val (affectedCount, remaining) = cancelRemoteUpdates(bundles.map { it.uid }.toSet())
707709 updateProgressAfterRemoval(affectedCount, remaining)
708710
709- State (sources.toPersistentMap(), info.toPersistentMap(), isLoaded = state.isLoaded )
711+ ready.copy (sources = sources .toPersistentMap(), info = info.toPersistentMap() )
710712 }
711713
712714 enum class DisplayNameUpdateResult {
@@ -753,9 +755,10 @@ class PatchBundleRepository(
753755
754756 if (result == DisplayNameUpdateResult .SUCCESS || result == DisplayNameUpdateResult .NO_CHANGE ) {
755757 dispatchAction(" Sync display name ($uid )" ) { state ->
756- val src = state.sources[uid] ? : return @dispatchAction state
758+ val ready = state as ? BundleState .Ready ? : return @dispatchAction state
759+ val src = ready.sources[uid] ? : return @dispatchAction state
757760 val updated = src.copy(displayName = normalized)
758- state .copy(sources = state .sources.put(uid, updated))
761+ ready .copy(sources = ready .sources.put(uid, updated))
759762 }
760763 }
761764
@@ -773,13 +776,14 @@ class PatchBundleRepository(
773776 prefs.bundlePrereleasesEnabled.update(current)
774777
775778 dispatchAction(" Set prerelease ($uid =$usePrerelease )" ) { state ->
776- val src = state.sources[uid] ? : return @dispatchAction state
779+ val ready = state as ? BundleState .Ready ? : return @dispatchAction state
780+ val src = ready.sources[uid] ? : return @dispatchAction state
777781 val updated = when (src) {
778782 is APIPatchBundle -> src.copy(usePrerelease = usePrerelease)
779783 is JsonPatchBundle -> src.copy(usePrerelease = usePrerelease)
780784 else -> return @dispatchAction state
781785 }
782- state .copy(sources = state .sources.put(uid, updated))
786+ ready .copy(sources = ready .sources.put(uid, updated))
783787 }
784788
785789 // If this is the default Morphe Patches bundle, sync FCM patches topic
@@ -794,7 +798,7 @@ class PatchBundleRepository(
794798
795799 // Skip download if the bundle is disabled - it will be downloaded when re-enabled
796800 // via disable() which triggers startRemoteUpdateJob for newly enabled bundles.
797- val isEnabled = store.state.value. sources[ uid] ?.enabled == true
801+ val isEnabled = ( store.state.value as ? BundleState . Ready )?. sources?.get( uid) ?.enabled == true
798802 if (! isEnabled) return
799803
800804 // Trigger update so the new channel takes effect immediately.
@@ -1049,7 +1053,9 @@ class PatchBundleRepository(
10491053
10501054
10511055 // Check for duplicate source
1052- val isDuplicate = state.sources.values.any { src ->
1056+ val ready = state as ? BundleState .Ready ? : return @dispatchAction state
1057+
1058+ val isDuplicate = ready.sources.values.any { src ->
10531059 src is RemotePatchBundle && src.endpoint.equals(normalizedUrl, ignoreCase = true )
10541060 }
10551061
@@ -1084,7 +1090,7 @@ class PatchBundleRepository(
10841090 if (bundle.uid == src.uid) onProgress?.invoke(bytesRead, bytesTotal)
10851091 }
10861092 )
1087- state .copy(sources = state .sources.put(src.uid, src))
1093+ ready .copy(sources = ready .sources.put(src.uid, src))
10881094 }
10891095
10901096 /* *
@@ -1315,7 +1321,8 @@ class PatchBundleRepository(
13151321 if (! allowMeteredUpdates && networkInfo.isMetered()) return null
13161322
13171323 return try {
1318- val remoteBundles = store.state.value.sources.values
1324+ val remoteBundles = (store.state.value as ? BundleState .Ready )?.sources?.values
1325+ .orEmpty()
13191326 .filterIsInstance<RemotePatchBundle >()
13201327
13211328 if (remoteBundles.isEmpty()) return null
@@ -1360,12 +1367,12 @@ class PatchBundleRepository(
13601367 private val allowUnsafeNetwork : Boolean = false ,
13611368 private val onPerBundleProgress : ((bundle: RemotePatchBundle , bytesRead: Long , bytesTotal: Long? ) -> Unit )? = null ,
13621369 private val predicate : (bundle: RemotePatchBundle ) -> Boolean = { true },
1363- ) : Action<State > {
1370+ ) : Action<BundleState > {
13641371 override fun toString () = if (force) " Redownload remote bundles" else " Update check"
13651372
13661373 override suspend fun ActionContext.execute (
1367- current : State
1368- ): State {
1374+ current : BundleState
1375+ ): BundleState {
13691376 startRemoteUpdateJob(
13701377 force = force,
13711378 showToast = showToast,
@@ -1476,7 +1483,8 @@ class PatchBundleRepository(
14761483 return @coroutineScope
14771484 }
14781485
1479- val targets = store.state.value.sources.values
1486+ val targets = (store.state.value as ? BundleState .Ready )?.sources?.values
1487+ .orEmpty()
14801488 .filterIsInstance<RemotePatchBundle >()
14811489 .filter { predicate(it) }
14821490
@@ -1666,9 +1674,10 @@ class PatchBundleRepository(
16661674
16671675 private inner class ManualUpdateCheck (
16681676 private val targetUids : Set <Int >? = null
1669- ) : Action<State> {
1670- override suspend fun ActionContext.execute (current : State ) = coroutineScope {
1671- val manualBundles = current.sources.values
1677+ ) : Action<BundleState> {
1678+ override suspend fun ActionContext.execute (current : BundleState ) = coroutineScope {
1679+ val ready = current as ? BundleState .Ready ? : return @coroutineScope current
1680+ val manualBundles = ready.sources.values
16721681 .filterIsInstance<RemotePatchBundle >()
16731682 .filter {
16741683 targetUids?.contains(it.uid) ? : ! it.autoUpdate
@@ -1680,7 +1689,7 @@ class PatchBundleRepository(
16801689 } else {
16811690 manualUpdateInfoFlow.update { map ->
16821691 map.filterKeys { uid ->
1683- val bundle = current .sources[uid] as ? RemotePatchBundle
1692+ val bundle = ready .sources[uid] as ? RemotePatchBundle
16841693 bundle != null && ! bundle.autoUpdate
16851694 }
16861695 }
@@ -1729,12 +1738,16 @@ class PatchBundleRepository(
17291738 }
17301739 }
17311740
1732- data class State (
1733- val sources : PersistentMap <Int , PatchBundleSource > = persistentMapOf(),
1734- val info : PersistentMap <Int , PatchBundleInfo .Global > = persistentMapOf(),
1735- /* * True once the initial DB load has completed (even if all bundles are disabled). */
1736- val isLoaded : Boolean = false
1737- )
1741+ sealed class BundleState {
1742+ /* * DB not yet read — UI shows shimmer */
1743+ data object Loading : BundleState ()
1744+
1745+ /* * Pipeline ready (even if sources list is empty) */
1746+ data class Ready (
1747+ val sources : PersistentMap <Int , PatchBundleSource > = persistentMapOf(),
1748+ val info : PersistentMap <Int , PatchBundleInfo .Global > = persistentMapOf(),
1749+ ) : BundleState()
1750+ }
17381751
17391752 enum class BundleUpdateResult {
17401753 None , // Update in progress
@@ -1810,7 +1823,8 @@ class PatchBundleRepository(
18101823 suspend fun importCustomBundles (snapshots : List <BundleSnapshot >) {
18111824 if (snapshots.isEmpty()) return
18121825 dispatchAction(" Import custom bundles" ) { state ->
1813- val existingEndpoints = state.sources.values
1826+ val ready = state as ? BundleState .Ready ? : return @dispatchAction state
1827+ val existingEndpoints = ready.sources.values
18141828 .filterIsInstance<RemotePatchBundle >()
18151829 .map { it.endpoint.lowercase(Locale .US ) }
18161830 .toSet()
0 commit comments