mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
feat: Allow offline mode in Purefin
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ConnectivityNetworkMonitor @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : NetworkMonitor {
|
||||
|
||||
override val isOnline: Flow<Boolean> = callbackFlow {
|
||||
val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
|
||||
|
||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(true)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
trySend(connectivityManager.isCurrentlyConnected())
|
||||
}
|
||||
}
|
||||
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
|
||||
trySend(connectivityManager.isCurrentlyConnected())
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
||||
}.distinctUntilChanged()
|
||||
|
||||
private fun ConnectivityManager.isCurrentlyConnected(): Boolean {
|
||||
val network = activeNetwork ?: return false
|
||||
val caps = getNetworkCapabilities(network) ?: return false
|
||||
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
val jellyfinApiClient: JellyfinApiClient,
|
||||
private val homeCacheDataStore: DataStore<HomeCache>,
|
||||
private val mediaRepository: InMemoryMediaRepository,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
) : AppContentRepository, MediaRepository by mediaRepository {
|
||||
|
||||
private val readyMutex = Mutex()
|
||||
@@ -65,7 +66,23 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
init {
|
||||
scope.launch {
|
||||
loadFromCache()
|
||||
runCatching { ensureReady() }
|
||||
networkMonitor.isOnline.collect { isOnline ->
|
||||
userSessionRepository.setOfflineMode(!isOnline)
|
||||
if (isOnline) {
|
||||
if (mediaRepository.ready.isCompleted) {
|
||||
// Reset so ensureReady() performs a fresh network load
|
||||
mediaRepository.reset()
|
||||
_state.value = MediaRepositoryState.Loading
|
||||
}
|
||||
runCatching { ensureReady() }
|
||||
} else {
|
||||
// Going offline – complete ready with cached data so waiters don't hang
|
||||
if (!mediaRepository.ready.isCompleted) {
|
||||
_state.value = MediaRepositoryState.Ready
|
||||
mediaRepository.signalReady()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +133,18 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun ensureReady() {
|
||||
val isOffline = userSessionRepository.isOfflineMode.first()
|
||||
if (isOffline) {
|
||||
// Offline mode: use cached data without network calls
|
||||
val ready = mediaRepository.ready
|
||||
if (!ready.isCompleted) {
|
||||
_state.value = MediaRepositoryState.Ready
|
||||
mediaRepository.signalReady()
|
||||
}
|
||||
mediaRepository.ready.await()
|
||||
return
|
||||
}
|
||||
|
||||
val ready = mediaRepository.ready
|
||||
if (ready.isCompleted) {
|
||||
ready.await()
|
||||
@@ -125,8 +154,8 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
// Only the first caller runs the loading logic; others wait on the deferred.
|
||||
if (readyMutex.tryLock()) {
|
||||
try {
|
||||
if (ready.isCompleted) {
|
||||
ready.await()
|
||||
if (mediaRepository.ready.isCompleted) {
|
||||
mediaRepository.ready.await()
|
||||
return
|
||||
}
|
||||
loadLibraries()
|
||||
@@ -145,7 +174,7 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
readyMutex.unlock()
|
||||
}
|
||||
} else {
|
||||
ready.await()
|
||||
mediaRepository.ready.await()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +317,9 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun refreshHomeData() {
|
||||
val isOffline = userSessionRepository.isOfflineMode.first()
|
||||
if (isOffline) return
|
||||
|
||||
mediaRepository.ready.await()
|
||||
// Skip refresh if the initial load (or last refresh) just happened
|
||||
val elapsed = System.currentTimeMillis() - initialLoadTimestamp
|
||||
|
||||
@@ -36,7 +36,11 @@ class InMemoryMediaRepository @Inject constructor(
|
||||
) : MediaRepository {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
internal val ready = CompletableDeferred<Unit>()
|
||||
@Volatile internal var ready = CompletableDeferred<Unit>()
|
||||
|
||||
internal fun reset() {
|
||||
ready = CompletableDeferred()
|
||||
}
|
||||
|
||||
internal val _movies: MutableStateFlow<Map<UUID, Movie>> = MutableStateFlow(emptyMap())
|
||||
override val movies: StateFlow<Map<UUID, Movie>> = _movies.asStateFlow()
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class NetworkModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindNetworkMonitor(impl: ConnectivityNetworkMonitor): NetworkMonitor
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkMonitor {
|
||||
val isOnline: Flow<Boolean>
|
||||
}
|
||||
Reference in New Issue
Block a user