mirror of
https://github.com/rafaelvcaetano/melonDS-android.git
synced 2024-11-26 23:20:40 +00:00
Migrate RomsRepository and RomListViewModel to Kotlin coroutines
This also fixes the slowdown when performing a ROM search when a large set of ROMs is present since the filtering is now performed outside of the UI thread
This commit is contained in:
parent
04dcd3e73a
commit
8ee5c80be8
@ -128,6 +128,7 @@ dependencies {
|
||||
implementation(flexbox)
|
||||
implementation(gson)
|
||||
implementation(hilt)
|
||||
implementation(kotlinxCoroutinesRx)
|
||||
implementation(picasso)
|
||||
implementation(markwon)
|
||||
implementation(markwonImagePicasso)
|
||||
|
@ -1,18 +1,18 @@
|
||||
package me.magnum.melonds.domain.repositories
|
||||
|
||||
import android.net.Uri
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Observable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.magnum.melonds.domain.model.Rom
|
||||
import me.magnum.melonds.domain.model.RomConfig
|
||||
import me.magnum.melonds.domain.model.RomScanningStatus
|
||||
import java.util.*
|
||||
|
||||
interface RomsRepository {
|
||||
fun getRoms(): Observable<List<Rom>>
|
||||
fun getRoms(): Flow<List<Rom>>
|
||||
fun getRomScanningStatus(): Observable<RomScanningStatus>
|
||||
fun getRomAtPath(path: String): Maybe<Rom>
|
||||
fun getRomAtUri(uri: Uri): Maybe<Rom>
|
||||
suspend fun getRomAtPath(path: String): Rom?
|
||||
suspend fun getRomAtUri(uri: Uri): Rom?
|
||||
|
||||
fun updateRomConfig(rom: Rom, romConfig: RomConfig)
|
||||
fun setRomLastPlayed(rom: Rom, lastPlayed: Date)
|
||||
|
@ -6,13 +6,15 @@ import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import io.reactivex.*
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.magnum.melonds.common.romprocessors.RomFileProcessorFactory
|
||||
import me.magnum.melonds.domain.model.Rom
|
||||
import me.magnum.melonds.domain.model.RomConfig
|
||||
@ -26,7 +28,7 @@ import java.io.FileReader
|
||||
import java.io.OutputStreamWriter
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class FileSystemRomsRepository(
|
||||
private val context: Context,
|
||||
@ -40,17 +42,21 @@ class FileSystemRomsRepository(
|
||||
private const val ROM_DATA_FILE = "rom_data.json"
|
||||
}
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
private val disposables = CompositeDisposable()
|
||||
private val romListType: Type = object : TypeToken<List<Rom>>(){}.type
|
||||
private val romsSubject: BehaviorSubject<List<Rom>> = BehaviorSubject.create()
|
||||
private val romsChannel: MutableSharedFlow<List<Rom>> = MutableSharedFlow(replay = 1, extraBufferCapacity = 0, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
private val scanningStatusSubject: BehaviorSubject<RomScanningStatus> = BehaviorSubject.createDefault(RomScanningStatus.NOT_SCANNING)
|
||||
private val roms: ArrayList<Rom> = ArrayList()
|
||||
private var areRomsLoaded = false
|
||||
private var areRomsLoaded = AtomicBoolean(false)
|
||||
|
||||
init {
|
||||
romsSubject.subscribeOn(Schedulers.io())
|
||||
.subscribe { roms -> saveRomData(roms) }
|
||||
.addTo(disposables)
|
||||
coroutineScope.launch {
|
||||
romsChannel.onEach {
|
||||
saveRomData(it)
|
||||
}.collect()
|
||||
}
|
||||
|
||||
settingsRepository.observeRomSearchDirectories()
|
||||
.subscribe { directories -> onRomSearchDirectoriesChanged(directories) }
|
||||
.addTo(disposables)
|
||||
@ -59,7 +65,7 @@ class FileSystemRomsRepository(
|
||||
private fun onRomSearchDirectoriesChanged(searchDirectories: Array<Uri>) {
|
||||
// If ROMs have not been loaded yet, there's no point in searching or discarding ROMs now.
|
||||
// They will be scanned once needed
|
||||
if (!areRomsLoaded)
|
||||
if (!areRomsLoaded.get())
|
||||
return
|
||||
|
||||
// TODO: Check if existing ROMs are still found in the new directory(s). How can we do that reliably using URIs?
|
||||
@ -67,35 +73,30 @@ class FileSystemRomsRepository(
|
||||
rescanRoms()
|
||||
}
|
||||
|
||||
override fun getRoms(): Observable<List<Rom>> {
|
||||
if (!areRomsLoaded) {
|
||||
areRomsLoaded = true
|
||||
loadCachedRoms()
|
||||
override fun getRoms(): Flow<List<Rom>> = flow {
|
||||
if (areRomsLoaded.compareAndSet(false, true)) {
|
||||
coroutineScope.launch {
|
||||
loadCachedRoms()
|
||||
}
|
||||
}
|
||||
return romsSubject
|
||||
emitAll(romsChannel)
|
||||
}
|
||||
|
||||
override fun getRomScanningStatus(): Observable<RomScanningStatus> {
|
||||
return scanningStatusSubject
|
||||
}
|
||||
|
||||
override fun getRomAtPath(path: String): Maybe<Rom> {
|
||||
return getRoms().firstElement()
|
||||
.flatMap {
|
||||
it.find { rom ->
|
||||
val romPath = FileUtils.getAbsolutePathFromSAFUri(context, rom.uri)
|
||||
romPath == path
|
||||
}?.let { rom -> Maybe.just(rom) } ?: Maybe.empty()
|
||||
}
|
||||
override suspend fun getRomAtPath(path: String): Rom? {
|
||||
return getRoms().first().find { rom ->
|
||||
val romPath = FileUtils.getAbsolutePathFromSAFUri(context, rom.uri)
|
||||
romPath == path
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRomAtUri(uri: Uri): Maybe<Rom> {
|
||||
return getRoms().firstElement()
|
||||
.flatMap {
|
||||
it.find { rom ->
|
||||
rom.uri == uri
|
||||
}?.let { rom -> Maybe.just(rom) } ?: Maybe.empty()
|
||||
}
|
||||
override suspend fun getRomAtUri(uri: Uri): Rom? {
|
||||
return getRoms().first().find { rom ->
|
||||
rom.uri == uri
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateRomConfig(rom: Rom, romConfig: RomConfig) {
|
||||
@ -118,28 +119,20 @@ class FileSystemRomsRepository(
|
||||
}
|
||||
|
||||
override fun rescanRoms() {
|
||||
scanForNewRoms()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(object : Observer<Rom> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
scanningStatusSubject.onNext(RomScanningStatus.SCANNING)
|
||||
}
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
scanningStatusSubject.onNext(RomScanningStatus.SCANNING)
|
||||
|
||||
override fun onNext(rom: Rom) {
|
||||
addRom(rom)
|
||||
}
|
||||
scanForNewRoms().collect {
|
||||
addRom(it)
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {}
|
||||
override fun onComplete() {
|
||||
scanningStatusSubject.onNext(RomScanningStatus.NOT_SCANNING)
|
||||
}
|
||||
})
|
||||
scanningStatusSubject.onNext(RomScanningStatus.NOT_SCANNING)
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidateRoms() {
|
||||
if (areRomsLoaded) {
|
||||
if (areRomsLoaded.compareAndSet(true, false)) {
|
||||
roms.clear()
|
||||
areRomsLoaded = false
|
||||
}
|
||||
|
||||
val cacheFile = File(context.filesDir, ROM_DATA_FILE)
|
||||
@ -168,85 +161,60 @@ class FileSystemRomsRepository(
|
||||
}
|
||||
|
||||
private fun onRomsChanged() {
|
||||
romsSubject.onNext(ArrayList(roms))
|
||||
romsChannel.tryEmit(roms)
|
||||
}
|
||||
|
||||
private fun loadCachedRoms() {
|
||||
getCachedRoms()
|
||||
.filter { rom -> DocumentFile.fromSingleUri(context, rom.uri)?.exists() == true }
|
||||
.toList()
|
||||
.doOnSuccess { cachedRoms ->
|
||||
roms.addAll(cachedRoms!!)
|
||||
onRomsChanged()
|
||||
}
|
||||
.flatMapObservable { scanForNewRoms() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(object : Observer<Rom> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
scanningStatusSubject.onNext(RomScanningStatus.SCANNING)
|
||||
}
|
||||
private suspend fun loadCachedRoms() = withContext(Dispatchers.IO) {
|
||||
scanningStatusSubject.onNext(RomScanningStatus.SCANNING)
|
||||
|
||||
override fun onNext(rom: Rom) {
|
||||
addRom(rom)
|
||||
}
|
||||
val cachedRoms = getCachedRoms().filter {
|
||||
DocumentFile.fromSingleUri(context, it.uri)?.exists() == true
|
||||
}.toCollection(mutableListOf())
|
||||
|
||||
override fun onError(e: Throwable) {}
|
||||
roms.addAll(cachedRoms)
|
||||
onRomsChanged()
|
||||
scanForNewRoms().collect {
|
||||
addRom(it)
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
scanningStatusSubject.onNext(RomScanningStatus.NOT_SCANNING)
|
||||
}
|
||||
})
|
||||
scanningStatusSubject.onNext(RomScanningStatus.NOT_SCANNING)
|
||||
}
|
||||
|
||||
private fun scanForNewRoms(): Observable<Rom> {
|
||||
return Observable.create(object : ObservableOnSubscribe<Rom> {
|
||||
private fun findFiles(directory: DocumentFile, emitter: ObservableEmitter<Rom>) {
|
||||
val files = directory.listFiles()
|
||||
for (file in files) {
|
||||
if (file.isDirectory) {
|
||||
findFiles(file, emitter)
|
||||
continue
|
||||
}
|
||||
|
||||
romFileProcessorFactory.getFileRomProcessorForDocument(file)?.let { fileRomProcessor ->
|
||||
fileRomProcessor.getRomFromUri(file.uri, directory.uri)?.let { emitter.onNext(it) }
|
||||
}
|
||||
}
|
||||
private fun scanForNewRoms(): Flow<Rom> = flow {
|
||||
for (directory in settingsRepository.getRomSearchDirectories()) {
|
||||
val documentFile = DocumentFile.fromTreeUri(context, directory)
|
||||
if (documentFile != null) {
|
||||
findCachedRomFiles(documentFile, this)
|
||||
}
|
||||
|
||||
override fun subscribe(emitter: ObservableEmitter<Rom>) {
|
||||
for (directory in settingsRepository.getRomSearchDirectories()) {
|
||||
val documentFile = DocumentFile.fromTreeUri(context, directory)
|
||||
if (documentFile != null) {
|
||||
findFiles(documentFile, emitter)
|
||||
}
|
||||
}
|
||||
|
||||
emitter.onComplete()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCachedRoms(): Observable<Rom> {
|
||||
return Observable.create(ObservableOnSubscribe { emitter ->
|
||||
val cacheFile = File(context.filesDir, ROM_DATA_FILE)
|
||||
if (!cacheFile.isFile) {
|
||||
emitter.onComplete()
|
||||
return@ObservableOnSubscribe
|
||||
private suspend fun findCachedRomFiles(directory: DocumentFile, collector: FlowCollector<Rom>) {
|
||||
val files = directory.listFiles()
|
||||
for (file in files) {
|
||||
if (file.isDirectory) {
|
||||
findCachedRomFiles(file, collector)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
val roms = gson.fromJson<List<Rom>>(FileReader(cacheFile), romListType)
|
||||
if (roms != null) {
|
||||
for (rom in roms) {
|
||||
emitter.onNext(rom)
|
||||
}
|
||||
}
|
||||
emitter.onComplete()
|
||||
} catch (_: Exception) {
|
||||
emitter.onComplete()
|
||||
romFileProcessorFactory.getFileRomProcessorForDocument(file)?.let { fileRomProcessor ->
|
||||
fileRomProcessor.getRomFromUri(file.uri, directory.uri)?.let { collector.emit(it) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCachedRoms(): Flow<Rom> = flow {
|
||||
val cacheFile = File(context.filesDir, ROM_DATA_FILE)
|
||||
if (!cacheFile.isFile) {
|
||||
return@flow
|
||||
}
|
||||
|
||||
try {
|
||||
gson.fromJson<List<Rom>>(FileReader(cacheFile), romListType)?.forEach {
|
||||
emit(it)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveRomData(romData: List<Rom>) {
|
||||
|
@ -11,6 +11,7 @@ import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.coroutines.rx2.rxMaybe
|
||||
import me.magnum.melonds.common.Schedulers
|
||||
import me.magnum.melonds.common.romprocessors.RomFileProcessorFactory
|
||||
import me.magnum.melonds.common.uridelegates.UriHandler
|
||||
@ -206,11 +207,15 @@ class EmulatorViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun getRomAtPath(path: String): Maybe<Rom> {
|
||||
return romsRepository.getRomAtPath(path)
|
||||
return rxMaybe {
|
||||
romsRepository.getRomAtPath(path)
|
||||
}
|
||||
}
|
||||
|
||||
fun getRomAtUri(uri: Uri): Maybe<Rom> {
|
||||
return romsRepository.getRomAtUri(uri)
|
||||
return rxMaybe {
|
||||
romsRepository.getRomAtUri(uri)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEmulatorConfigurationForRom(rom: Rom): EmulatorConfiguration {
|
||||
|
@ -15,8 +15,10 @@ import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.noties.markwon.Markwon
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import me.magnum.melonds.R
|
||||
import me.magnum.melonds.common.Permission
|
||||
import me.magnum.melonds.common.contracts.DirectoryPickerContract
|
||||
@ -71,15 +73,22 @@ class RomListActivity : AppCompatActivity() {
|
||||
val binding = ActivityRomListBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
viewModel.hasRomScanningDirectories().observe(this) { hasDirectories ->
|
||||
if (hasDirectories)
|
||||
addRomListFragment()
|
||||
else
|
||||
addNoSearchDirectoriesFragment()
|
||||
lifecycleScope.launchWhenStarted {
|
||||
viewModel.hasSearchDirectories.collectLatest { hasDirectories ->
|
||||
if (hasDirectories) {
|
||||
addRomListFragment()
|
||||
} else {
|
||||
addNoSearchDirectoriesFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel.invalidDirectoryAccessEvent.observe(this) {
|
||||
showInvalidDirectoryAccessDialog()
|
||||
|
||||
lifecycleScope.launchWhenStarted {
|
||||
viewModel.invalidDirectoryAccessEvent.collectLatest {
|
||||
showInvalidDirectoryAccessDialog()
|
||||
}
|
||||
}
|
||||
|
||||
updatesViewModel.getAppUpdate().observe(this) {
|
||||
showUpdateAvailableDialog(it)
|
||||
}
|
||||
|
@ -12,12 +12,14 @@ import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import me.magnum.melonds.R
|
||||
import me.magnum.melonds.databinding.ItemRomConfigurableBinding
|
||||
import me.magnum.melonds.databinding.ItemRomSimpleBinding
|
||||
@ -26,8 +28,6 @@ import me.magnum.melonds.domain.model.Rom
|
||||
import me.magnum.melonds.domain.model.RomIconFiltering
|
||||
import me.magnum.melonds.domain.model.RomScanningStatus
|
||||
import me.magnum.melonds.ui.romlist.RomListFragment.RomListAdapter.RomViewHolder
|
||||
import me.magnum.melonds.utils.FileUtils
|
||||
import java.util.*
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RomListFragment : Fragment() {
|
||||
@ -78,13 +78,18 @@ class RomListFragment : Fragment() {
|
||||
adapter = romListAdapter
|
||||
}
|
||||
|
||||
romListViewModel.getRomScanningStatus().observe(viewLifecycleOwner) { status ->
|
||||
binding.swipeRefreshRoms.isRefreshing = status == RomScanningStatus.SCANNING
|
||||
displayEmptyListViewIfRequired()
|
||||
lifecycleScope.launchWhenStarted {
|
||||
romListViewModel.romScanningStatus.collectLatest { status ->
|
||||
binding.swipeRefreshRoms.isRefreshing = status == RomScanningStatus.SCANNING
|
||||
displayEmptyListViewIfRequired()
|
||||
}
|
||||
}
|
||||
romListViewModel.getRoms().observe(viewLifecycleOwner) { roms ->
|
||||
romListAdapter.setRoms(roms)
|
||||
displayEmptyListViewIfRequired()
|
||||
|
||||
lifecycleScope.launchWhenStarted {
|
||||
romListViewModel.roms.collectLatest { roms ->
|
||||
romListAdapter.setRoms(roms)
|
||||
displayEmptyListViewIfRequired()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
package me.magnum.melonds.ui.romlist
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.magnum.melonds.common.DirectoryAccessValidator
|
||||
import me.magnum.melonds.common.Permission
|
||||
import me.magnum.melonds.common.Schedulers
|
||||
@ -20,7 +22,8 @@ import me.magnum.melonds.domain.repositories.SettingsRepository
|
||||
import me.magnum.melonds.domain.services.ConfigurationDirectoryVerifier
|
||||
import me.magnum.melonds.extensions.addTo
|
||||
import me.magnum.melonds.impl.RomIconProvider
|
||||
import me.magnum.melonds.utils.SingleLiveEvent
|
||||
import me.magnum.melonds.utils.EventSharedFlow
|
||||
import me.magnum.melonds.utils.SubjectSharedFlow
|
||||
import java.text.Normalizer
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
@ -40,70 +43,67 @@ class RomListViewModel @Inject constructor(
|
||||
|
||||
private val disposables: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
private val _invalidDirectoryAccessEvent = SingleLiveEvent<Unit>()
|
||||
val invalidDirectoryAccessEvent: LiveData<Unit> = _invalidDirectoryAccessEvent
|
||||
private val _searchQuery = MutableStateFlow("")
|
||||
private val _sortingMode = MutableStateFlow(settingsRepository.getRomSortingMode())
|
||||
private val _sortingOrder = MutableStateFlow(settingsRepository.getRomSortingOrder())
|
||||
|
||||
private val romsLiveData = MutableLiveData<List<Rom>>()
|
||||
private val hasSearchDirectoriesLiveData = MutableLiveData<Boolean>()
|
||||
private val romsFilteredLiveData: MediatorLiveData<List<Rom>>
|
||||
private val _hasSearchDirectories = SubjectSharedFlow<Boolean>()
|
||||
val hasSearchDirectories: Flow<Boolean> = _hasSearchDirectories
|
||||
|
||||
private var romSearchQuery = ""
|
||||
private var sortingMode = settingsRepository.getRomSortingMode()
|
||||
private var sortingOrder = settingsRepository.getRomSortingOrder()
|
||||
private val _invalidDirectoryAccessEvent = EventSharedFlow<Unit>()
|
||||
val invalidDirectoryAccessEvent: Flow<Unit> = _invalidDirectoryAccessEvent
|
||||
|
||||
private val _romScanningStatus = MutableStateFlow(RomScanningStatus.NOT_SCANNING)
|
||||
val romScanningStatus = _romScanningStatus.asStateFlow()
|
||||
|
||||
private val _roms = MutableStateFlow<List<Rom>>(emptyList())
|
||||
val roms = _roms.asStateFlow()
|
||||
|
||||
init {
|
||||
settingsRepository.observeRomIconFiltering()
|
||||
.subscribe { romsLiveData.postValue(romsLiveData.value) }
|
||||
.addTo(disposables)
|
||||
|
||||
settingsRepository.observeRomSearchDirectories()
|
||||
.startWith(settingsRepository.getRomSearchDirectories())
|
||||
.distinctUntilChanged()
|
||||
.subscribe { directories -> hasSearchDirectoriesLiveData.postValue(directories.isNotEmpty()) }
|
||||
.subscribe { directories -> _hasSearchDirectories.tryEmit(directories.isNotEmpty()) }
|
||||
.addTo(disposables)
|
||||
|
||||
romsFilteredLiveData = MediatorLiveData<List<Rom>>().apply {
|
||||
addSource(romsLiveData) {
|
||||
val romList = if (romSearchQuery.isEmpty()) {
|
||||
it
|
||||
} else {
|
||||
it?.filter { rom ->
|
||||
settingsRepository.observeRomIconFiltering()
|
||||
.subscribe { _roms.value = _roms.value }
|
||||
.addTo(disposables)
|
||||
|
||||
romsRepository.getRomScanningStatus()
|
||||
.subscribe { status -> _romScanningStatus.value = status }
|
||||
.addTo(disposables)
|
||||
|
||||
combine(romsRepository.getRoms(), _searchQuery) { roms, query ->
|
||||
val romList = if (query.isEmpty()) {
|
||||
roms
|
||||
} else {
|
||||
withContext(Dispatchers.Default) {
|
||||
roms.filter { rom ->
|
||||
if (!isActive) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
|
||||
val normalizedName = Normalizer.normalize(rom.name, Normalizer.Form.NFD).replace("[^\\p{ASCII}]", "")
|
||||
val normalizedPath = Normalizer.normalize(uriHandler.getUriDocument(rom.uri)?.name, Normalizer.Form.NFD).replace("[^\\p{ASCII}]", "")
|
||||
|
||||
normalizedName.contains(romSearchQuery, true) || normalizedPath.contains(romSearchQuery, true)
|
||||
}
|
||||
}
|
||||
|
||||
if (romList != null) {
|
||||
value = when (sortingMode) {
|
||||
SortingMode.ALPHABETICALLY -> romList.sortedWith(buildAlphabeticalRomComparator())
|
||||
SortingMode.RECENTLY_PLAYED -> romList.sortedWith(buildRecentlyPlayedRomComparator())
|
||||
normalizedName.contains(query, true) || normalizedPath.contains(query, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
romsRepository.getRoms()
|
||||
.subscribeOn(schedulers.backgroundThreadScheduler)
|
||||
.subscribe { roms -> romsLiveData.postValue(roms) }
|
||||
.addTo(disposables)
|
||||
}
|
||||
_roms.value = when (_sortingMode.value) {
|
||||
SortingMode.ALPHABETICALLY -> romList.sortedWith(buildAlphabeticalRomComparator(_sortingOrder.value))
|
||||
SortingMode.RECENTLY_PLAYED -> romList.sortedWith(buildRecentlyPlayedRomComparator(_sortingOrder.value))
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
fun hasRomScanningDirectories(): LiveData<Boolean> {
|
||||
return hasSearchDirectoriesLiveData
|
||||
}
|
||||
|
||||
fun getRoms(): LiveData<List<Rom>> {
|
||||
return romsFilteredLiveData
|
||||
}
|
||||
|
||||
fun getRomScanningStatus(): LiveData<RomScanningStatus> {
|
||||
val scanningStatusLiveData = MutableLiveData<RomScanningStatus>()
|
||||
val disposable = romsRepository.getRomScanningStatus()
|
||||
.subscribe { status -> scanningStatusLiveData.postValue(status) }
|
||||
disposables.add(disposable)
|
||||
return scanningStatusLiveData
|
||||
combine(_sortingMode, _sortingOrder) { sortingMode, sortingOrder ->
|
||||
_roms.value = when (sortingMode) {
|
||||
SortingMode.ALPHABETICALLY -> _roms.value.sortedWith(buildAlphabeticalRomComparator(sortingOrder))
|
||||
SortingMode.RECENTLY_PLAYED -> _roms.value.sortedWith(buildRecentlyPlayedRomComparator(sortingOrder))
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun refreshRoms() {
|
||||
@ -129,27 +129,25 @@ class RomListViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun setRomSearchQuery(query: String?) {
|
||||
romSearchQuery = Normalizer.normalize(query ?: "", Normalizer.Form.NFD).replace("[^\\p{ASCII}]", "")
|
||||
romsLiveData.value = romsLiveData.value
|
||||
_searchQuery.tryEmit(Normalizer.normalize(query ?: "", Normalizer.Form.NFD).replace("[^\\p{ASCII}]", ""))
|
||||
}
|
||||
|
||||
fun setRomSorting(sortingMode: SortingMode) {
|
||||
if (sortingMode == this.sortingMode) {
|
||||
sortingOrder = if (sortingOrder == SortingOrder.ASCENDING)
|
||||
if (sortingMode == _sortingMode.value) {
|
||||
val newSortingOrder = if (_sortingOrder.value == SortingOrder.ASCENDING)
|
||||
SortingOrder.DESCENDING
|
||||
else
|
||||
SortingOrder.ASCENDING
|
||||
|
||||
settingsRepository.setRomSortingOrder(sortingOrder)
|
||||
settingsRepository.setRomSortingOrder(_sortingOrder.value)
|
||||
_sortingOrder.value = newSortingOrder
|
||||
} else {
|
||||
this.sortingMode = sortingMode
|
||||
sortingOrder = sortingMode.defaultOrder
|
||||
|
||||
settingsRepository.setRomSortingMode(sortingMode)
|
||||
settingsRepository.setRomSortingOrder(sortingOrder)
|
||||
}
|
||||
settingsRepository.setRomSortingOrder(sortingMode.defaultOrder)
|
||||
|
||||
romsLiveData.value = romsLiveData.value
|
||||
_sortingMode.value = sortingMode
|
||||
_sortingOrder.value = sortingMode.defaultOrder
|
||||
}
|
||||
}
|
||||
|
||||
fun getConsoleConfigurationDirResult(consoleType: ConsoleType): ConfigurationDirResult {
|
||||
@ -173,7 +171,7 @@ class RomListViewModel @Inject constructor(
|
||||
uriPermissionManager.persistDirectoryPermissions(directoryUri, Permission.READ_WRITE)
|
||||
settingsRepository.addRomSearchDirectory(directoryUri)
|
||||
} else {
|
||||
_invalidDirectoryAccessEvent.call()
|
||||
_invalidDirectoryAccessEvent.tryEmit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
@ -190,7 +188,7 @@ class RomListViewModel @Inject constructor(
|
||||
settingsRepository.setDsBiosDirectory(uri)
|
||||
true
|
||||
} else {
|
||||
_invalidDirectoryAccessEvent.call()
|
||||
_invalidDirectoryAccessEvent.tryEmit(Unit)
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -208,7 +206,7 @@ class RomListViewModel @Inject constructor(
|
||||
settingsRepository.setDsiBiosDirectory(uri)
|
||||
true
|
||||
} else {
|
||||
_invalidDirectoryAccessEvent.call()
|
||||
_invalidDirectoryAccessEvent.tryEmit(Unit)
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -229,7 +227,7 @@ class RomListViewModel @Inject constructor(
|
||||
return uriHandler.getUriDocument(uri)?.name
|
||||
}
|
||||
|
||||
private fun buildAlphabeticalRomComparator(): Comparator<Rom> {
|
||||
private fun buildAlphabeticalRomComparator(sortingOrder: SortingOrder): Comparator<Rom> {
|
||||
return if (sortingOrder == SortingOrder.ASCENDING) {
|
||||
Comparator { o1: Rom, o2: Rom ->
|
||||
o1.name.compareTo(o2.name)
|
||||
@ -241,7 +239,7 @@ class RomListViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRecentlyPlayedRomComparator(): Comparator<Rom> {
|
||||
private fun buildRecentlyPlayedRomComparator(sortingOrder: SortingOrder): Comparator<Rom> {
|
||||
return if (sortingOrder == SortingOrder.ASCENDING) {
|
||||
Comparator { o1: Rom, o2: Rom ->
|
||||
when {
|
||||
|
16
app/src/main/java/me/magnum/melonds/utils/SharedFlow.kt
Normal file
16
app/src/main/java/me/magnum/melonds/utils/SharedFlow.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package me.magnum.melonds.utils
|
||||
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
/**
|
||||
* Creates a [MutableSharedFlow] that holds a single value and has no initial value.
|
||||
*/
|
||||
@Suppress("FunctionName", "UNCHECKED_CAST")
|
||||
fun <T> SubjectSharedFlow() = MutableSharedFlow<T>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
/**
|
||||
* Creates a [MutableSharedFlow] that doesn't hold any value. Suitable to create flows used to fire events.
|
||||
*/
|
||||
@Suppress("FunctionName", "UNCHECKED_CAST")
|
||||
fun <T> EventSharedFlow() = MutableSharedFlow<T>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
@ -16,6 +16,7 @@ object Dependencies {
|
||||
const val Hilt = "2.38.1"
|
||||
const val Junit = "4.12"
|
||||
const val Kotlin = "1.5.10"
|
||||
const val KotlinxCoroutinesRx = "1.6.4"
|
||||
const val LifecycleExtensions = "2.0.0"
|
||||
const val LifecycleViewModel = "2.3.1"
|
||||
const val MasterSwitchPreference = "0.9.4"
|
||||
@ -75,6 +76,7 @@ object Dependencies {
|
||||
const val flexbox = "com.google.android.flexbox:flexbox:${Versions.Flexbox}"
|
||||
const val gson = "com.google.code.gson:gson:${Versions.Gson}"
|
||||
const val hilt = "com.google.dagger:hilt-android:${Versions.Hilt}"
|
||||
const val kotlinxCoroutinesRx = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:${Versions.KotlinxCoroutinesRx}"
|
||||
const val picasso = "com.squareup.picasso:picasso:${Versions.Picasso}"
|
||||
const val markwon = "io.noties.markwon:core:${Versions.Markwon}"
|
||||
const val markwonImagePicasso = "io.noties.markwon:image-picasso:${Versions.Markwon}"
|
||||
|
Loading…
Reference in New Issue
Block a user