🛠️ Fix not thread-safe _state.update() calls
* Fixed multiple issues with update calls being executed simultaneously
This commit is contained in:
parent
a25cb8f3eb
commit
bc9c378e43
9 changed files with 344 additions and 226 deletions
|
|
@ -10,8 +10,9 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.use_case.remote.CheckForUpdates
|
import ua.acclorite.book_story.domain.use_case.remote.CheckForUpdates
|
||||||
|
|
@ -25,6 +26,8 @@ class AboutModel @Inject constructor(
|
||||||
private val checkForUpdates: CheckForUpdates
|
private val checkForUpdates: CheckForUpdates
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val _state = MutableStateFlow(AboutState())
|
private val _state = MutableStateFlow(AboutState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -100,10 +103,18 @@ class AboutModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is AboutEvent.OnDismissDialog -> {
|
is AboutEvent.OnDismissDialog -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(dialog = null)
|
_state.update {
|
||||||
|
it.copy(dialog = null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||||
|
mutex.withLock {
|
||||||
|
this.value = function(this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18,8 +18,9 @@ import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
|
|
@ -55,6 +56,8 @@ class BookInfoModel @Inject constructor(
|
||||||
private val updateBookWithText: UpdateBookWithText
|
private val updateBookWithText: UpdateBookWithText
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val _state = MutableStateFlow(BookInfoState())
|
private val _state = MutableStateFlow(BookInfoState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -384,12 +387,13 @@ class BookInfoModel @Inject constructor(
|
||||||
|
|
||||||
is BookInfoEvent.OnCheckCoverReset -> {
|
is BookInfoEvent.OnCheckCoverReset -> {
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
_state.update {
|
if (_state.value.book.id == -1) return@launch
|
||||||
it.copy(
|
canResetCover.execute(_state.value.book.id).apply {
|
||||||
canResetCover = canResetCover.execute(
|
_state.update {
|
||||||
_state.value.book.id
|
it.copy(
|
||||||
|
canResetCover = this
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -780,16 +784,16 @@ class BookInfoModel @Inject constructor(
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventJob.cancel()
|
||||||
|
eventJob.join()
|
||||||
|
eventJob = SupervisorJob()
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
BookInfoState(
|
BookInfoState(
|
||||||
book = book
|
book = book
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
eventJob.cancel()
|
|
||||||
eventJob.join()
|
|
||||||
eventJob = SupervisorJob()
|
|
||||||
|
|
||||||
if (startUpdate) {
|
if (startUpdate) {
|
||||||
onEvent(
|
onEvent(
|
||||||
BookInfoEvent.OnCheckForTextUpdate(
|
BookInfoEvent.OnCheckForTextUpdate(
|
||||||
|
|
@ -804,13 +808,14 @@ class BookInfoModel @Inject constructor(
|
||||||
|
|
||||||
fun resetScreen() {
|
fun resetScreen() {
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
_state.update {
|
|
||||||
BookInfoState()
|
|
||||||
}
|
|
||||||
|
|
||||||
eventJob.cancel()
|
eventJob.cancel()
|
||||||
eventJob.join()
|
|
||||||
eventJob = SupervisorJob()
|
eventJob = SupervisorJob()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||||
|
mutex.withLock {
|
||||||
|
this.value = function(this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18,8 +18,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
|
|
@ -47,6 +48,8 @@ class BrowseModel @Inject constructor(
|
||||||
private val insertBook: InsertBook
|
private val insertBook: InsertBook
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val _state = MutableStateFlow(BrowseState())
|
private val _state = MutableStateFlow(BrowseState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -119,27 +122,31 @@ class BrowseModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnRequestFocus -> {
|
is BrowseEvent.OnRequestFocus -> {
|
||||||
if (!_state.value.hasFocused) {
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
event.focusRequester.requestFocus()
|
if (!_state.value.hasFocused) {
|
||||||
_state.update {
|
event.focusRequester.requestFocus()
|
||||||
it.copy(
|
_state.update {
|
||||||
hasFocused = true
|
it.copy(
|
||||||
)
|
hasFocused = true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnSearchQueryChange -> {
|
is BrowseEvent.OnSearchQueryChange -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(
|
_state.update {
|
||||||
searchQuery = event.query
|
it.copy(
|
||||||
)
|
searchQuery = event.query
|
||||||
}
|
)
|
||||||
changeSearchQueryJob?.cancel()
|
}
|
||||||
changeSearchQueryJob = viewModelScope.launch(Dispatchers.IO) {
|
changeSearchQueryJob?.cancel()
|
||||||
delay(500)
|
changeSearchQueryJob = launch(Dispatchers.IO) {
|
||||||
yield()
|
delay(500)
|
||||||
onEvent(BrowseEvent.OnSearch)
|
yield()
|
||||||
|
onEvent(BrowseEvent.OnSearch)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,18 +299,22 @@ class BrowseModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnShowFilterBottomSheet -> {
|
is BrowseEvent.OnShowFilterBottomSheet -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(
|
_state.update {
|
||||||
bottomSheet = BrowseScreen.FILTER_BOTTOM_SHEET
|
it.copy(
|
||||||
)
|
bottomSheet = BrowseScreen.FILTER_BOTTOM_SHEET
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnDismissBottomSheet -> {
|
is BrowseEvent.OnDismissBottomSheet -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(
|
_state.update {
|
||||||
bottomSheet = null
|
it.copy(
|
||||||
)
|
bottomSheet = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,20 +424,22 @@ class BrowseModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnDismissPermissionDialog -> {
|
is BrowseEvent.OnDismissPermissionDialog -> {
|
||||||
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
viewModelScope.launch {
|
||||||
val isPermissionGranted = if (!legacyPermission) {
|
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||||
Environment.isExternalStorageManager()
|
val isPermissionGranted = if (!legacyPermission) {
|
||||||
} else event.storagePermissionState.status.isGranted
|
Environment.isExternalStorageManager()
|
||||||
|
} else event.storagePermissionState.status.isGranted
|
||||||
|
|
||||||
storagePermissionJob?.cancel()
|
storagePermissionJob?.cancel()
|
||||||
_state.update { it.copy(dialog = null) }
|
_state.update { it.copy(dialog = null) }
|
||||||
|
|
||||||
if (isPermissionGranted) {
|
if (isPermissionGranted) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
getFilesFromDownloads()
|
getFilesFromDownloads()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update { it.copy(isError = true) }
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
_state.update { it.copy(isError = true) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,12 +497,14 @@ class BrowseModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnDismissAddDialog -> {
|
is BrowseEvent.OnDismissAddDialog -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(
|
_state.update {
|
||||||
dialog = null
|
it.copy(
|
||||||
)
|
dialog = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getAddDialogBooksJob?.cancel()
|
||||||
}
|
}
|
||||||
getAddDialogBooksJob?.cancel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnActionAddDialog -> {
|
is BrowseEvent.OnActionAddDialog -> {
|
||||||
|
|
@ -574,10 +589,12 @@ class BrowseModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnDismissDialog -> {
|
is BrowseEvent.OnDismissDialog -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(
|
_state.update {
|
||||||
dialog = null
|
it.copy(
|
||||||
)
|
dialog = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -601,8 +618,10 @@ class BrowseModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetScreen() {
|
fun resetScreen() {
|
||||||
storagePermissionJob?.cancel()
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isError = false) }
|
storagePermissionJob?.cancel()
|
||||||
|
_state.update { it.copy(isError = false) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun filterList(
|
fun filterList(
|
||||||
|
|
@ -710,4 +729,10 @@ class BrowseModel @Inject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||||
|
mutex.withLock {
|
||||||
|
this.value = function(this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,8 +11,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
|
|
@ -42,6 +43,8 @@ class HistoryModel @Inject constructor(
|
||||||
private val deleteWholeHistory: DeleteWholeHistory
|
private val deleteWholeHistory: DeleteWholeHistory
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val _state = MutableStateFlow(HistoryState())
|
private val _state = MutableStateFlow(HistoryState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -136,27 +139,31 @@ class HistoryModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is HistoryEvent.OnRequestFocus -> {
|
is HistoryEvent.OnRequestFocus -> {
|
||||||
if (!_state.value.hasFocused) {
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
event.focusRequester.requestFocus()
|
if (!_state.value.hasFocused) {
|
||||||
_state.update {
|
event.focusRequester.requestFocus()
|
||||||
it.copy(
|
_state.update {
|
||||||
hasFocused = true
|
it.copy(
|
||||||
)
|
hasFocused = true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is HistoryEvent.OnSearchQueryChange -> {
|
is HistoryEvent.OnSearchQueryChange -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(
|
_state.update {
|
||||||
searchQuery = event.query
|
it.copy(
|
||||||
)
|
searchQuery = event.query
|
||||||
}
|
)
|
||||||
searchQueryChange?.cancel()
|
}
|
||||||
searchQueryChange = viewModelScope.launch(Dispatchers.IO) {
|
searchQueryChange?.cancel()
|
||||||
delay(500)
|
searchQueryChange = launch(Dispatchers.IO) {
|
||||||
yield()
|
delay(500)
|
||||||
onEvent(HistoryEvent.OnSearch)
|
yield()
|
||||||
|
onEvent(HistoryEvent.OnSearch)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,10 +216,12 @@ class HistoryModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is HistoryEvent.OnShowDeleteWholeHistoryDialog -> {
|
is HistoryEvent.OnShowDeleteWholeHistoryDialog -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(
|
_state.update {
|
||||||
dialog = HistoryScreen.DELETE_WHOLE_HISTORY_DIALOG
|
it.copy(
|
||||||
)
|
dialog = HistoryScreen.DELETE_WHOLE_HISTORY_DIALOG
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,10 +247,12 @@ class HistoryModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is HistoryEvent.OnDismissDialog -> {
|
is HistoryEvent.OnDismissDialog -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(
|
_state.update {
|
||||||
dialog = null
|
it.copy(
|
||||||
)
|
dialog = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -314,4 +325,10 @@ class HistoryModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||||
|
mutex.withLock {
|
||||||
|
this.value = function(this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -10,8 +10,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
|
|
@ -32,6 +33,8 @@ class LibraryModel @Inject constructor(
|
||||||
private val moveBooks: UpdateBook
|
private val moveBooks: UpdateBook
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val _state = MutableStateFlow(LibraryState())
|
private val _state = MutableStateFlow(LibraryState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -104,16 +107,18 @@ class LibraryModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is LibraryEvent.OnSearchQueryChange -> {
|
is LibraryEvent.OnSearchQueryChange -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(
|
_state.update {
|
||||||
searchQuery = event.query
|
it.copy(
|
||||||
)
|
searchQuery = event.query
|
||||||
}
|
)
|
||||||
searchQueryChange?.cancel()
|
}
|
||||||
searchQueryChange = viewModelScope.launch(Dispatchers.IO) {
|
searchQueryChange?.cancel()
|
||||||
delay(500)
|
searchQueryChange = launch(Dispatchers.IO) {
|
||||||
yield()
|
delay(500)
|
||||||
onEvent(LibraryEvent.OnSearch)
|
yield()
|
||||||
|
onEvent(LibraryEvent.OnSearch)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,12 +129,14 @@ class LibraryModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is LibraryEvent.OnRequestFocus -> {
|
is LibraryEvent.OnRequestFocus -> {
|
||||||
if (!_state.value.hasFocused) {
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
event.focusRequester.requestFocus()
|
if (!_state.value.hasFocused) {
|
||||||
_state.update {
|
event.focusRequester.requestFocus()
|
||||||
it.copy(
|
_state.update {
|
||||||
hasFocused = true
|
it.copy(
|
||||||
)
|
hasFocused = true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -163,10 +170,12 @@ class LibraryModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is LibraryEvent.OnShowMoveDialog -> {
|
is LibraryEvent.OnShowMoveDialog -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(
|
_state.update {
|
||||||
dialog = LibraryScreen.MOVE_DIALOG
|
it.copy(
|
||||||
)
|
dialog = LibraryScreen.MOVE_DIALOG
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,10 +222,12 @@ class LibraryModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is LibraryEvent.OnShowDeleteDialog -> {
|
is LibraryEvent.OnShowDeleteDialog -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(
|
_state.update {
|
||||||
dialog = LibraryScreen.DELETE_DIALOG
|
it.copy(
|
||||||
)
|
dialog = LibraryScreen.DELETE_DIALOG
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,10 +260,12 @@ class LibraryModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is LibraryEvent.OnDismissDialog -> {
|
is LibraryEvent.OnDismissDialog -> {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
it.copy(
|
_state.update {
|
||||||
dialog = null
|
it.copy(
|
||||||
)
|
dialog = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -273,4 +286,10 @@ class LibraryModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||||
|
mutex.withLock {
|
||||||
|
this.value = function(this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,8 +11,9 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import ua.acclorite.book_story.domain.browse.toBrowseFilesStructure
|
import ua.acclorite.book_story.domain.browse.toBrowseFilesStructure
|
||||||
import ua.acclorite.book_story.domain.browse.toBrowseLayout
|
import ua.acclorite.book_story.domain.browse.toBrowseLayout
|
||||||
|
|
@ -45,6 +46,8 @@ class MainModel @Inject constructor(
|
||||||
private val getAllSettings: GetAllSettings
|
private val getAllSettings: GetAllSettings
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val _isReady = MutableStateFlow(false)
|
private val _isReady = MutableStateFlow(false)
|
||||||
val isReady = _isReady.asStateFlow()
|
val isReady = _isReady.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -506,4 +509,10 @@ class MainModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||||
|
mutex.withLock {
|
||||||
|
this.value = function(this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -22,8 +22,9 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
|
|
@ -54,6 +55,8 @@ class ReaderModel @Inject constructor(
|
||||||
private val checkForTextUpdate: CheckForTextUpdate
|
private val checkForTextUpdate: CheckForTextUpdate
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val _state = MutableStateFlow(ReaderState())
|
private val _state = MutableStateFlow(ReaderState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -550,13 +553,14 @@ class ReaderModel @Inject constructor(
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventJob.cancel()
|
||||||
|
eventJob.join()
|
||||||
|
eventJob = SupervisorJob()
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
ReaderState(book = book)
|
ReaderState(book = book)
|
||||||
}
|
}
|
||||||
|
|
||||||
eventJob.cancel()
|
|
||||||
eventJob = SupervisorJob()
|
|
||||||
|
|
||||||
onEvent(
|
onEvent(
|
||||||
ReaderEvent.OnMenuVisibility(
|
ReaderEvent.OnMenuVisibility(
|
||||||
show = false,
|
show = false,
|
||||||
|
|
@ -606,12 +610,14 @@ class ReaderModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateChapter(index: Int) {
|
private fun updateChapter(index: Int) {
|
||||||
val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index)
|
viewModelScope.launch {
|
||||||
_state.update {
|
val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index)
|
||||||
it.copy(
|
_state.update {
|
||||||
currentChapter = currentChapter,
|
it.copy(
|
||||||
currentChapterProgress = currentChapterProgress
|
currentChapter = currentChapter,
|
||||||
)
|
currentChapterProgress = currentChapterProgress
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -672,12 +678,14 @@ class ReaderModel @Inject constructor(
|
||||||
|
|
||||||
fun resetScreen() {
|
fun resetScreen() {
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
_state.update {
|
|
||||||
ReaderState()
|
|
||||||
}
|
|
||||||
|
|
||||||
eventJob.cancel()
|
eventJob.cancel()
|
||||||
eventJob = SupervisorJob()
|
eventJob = SupervisorJob()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||||
|
mutex.withLock {
|
||||||
|
this.value = function(this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -15,8 +15,9 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
|
|
@ -42,6 +43,8 @@ class SettingsModel @Inject constructor(
|
||||||
private val deleteColorPreset: DeleteColorPreset
|
private val deleteColorPreset: DeleteColorPreset
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val _state = MutableStateFlow(SettingsState())
|
private val _state = MutableStateFlow(SettingsState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -536,4 +539,10 @@ class SettingsModel @Inject constructor(
|
||||||
updateColorColorPresetJob?.cancel()
|
updateColorColorPresetJob?.cancel()
|
||||||
deleteColorPresetJob?.cancel()
|
deleteColorPresetJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||||
|
mutex.withLock {
|
||||||
|
this.value = function(this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,8 +16,9 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import ua.acclorite.book_story.presentation.core.util.launchActivity
|
import ua.acclorite.book_story.presentation.core.util.launchActivity
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -27,6 +28,8 @@ class StartModel @Inject constructor(
|
||||||
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val _state = MutableStateFlow(StartState())
|
private val _state = MutableStateFlow(StartState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -59,110 +62,114 @@ class StartModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is StartEvent.OnStoragePermissionRequest -> {
|
is StartEvent.OnStoragePermissionRequest -> {
|
||||||
val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
viewModelScope.launch {
|
||||||
|
val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||||
|
|
||||||
val isPermissionGranted = if (legacyStoragePermission) {
|
val isPermissionGranted = if (legacyStoragePermission) {
|
||||||
event.storagePermissionState.status.isGranted
|
event.storagePermissionState.status.isGranted
|
||||||
} else Environment.isExternalStorageManager()
|
} else Environment.isExternalStorageManager()
|
||||||
|
|
||||||
if (isPermissionGranted) {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
storagePermissionGranted = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (legacyStoragePermission) {
|
|
||||||
if (!event.storagePermissionState.status.shouldShowRationale) {
|
|
||||||
event.storagePermissionState.launchPermissionRequest()
|
|
||||||
} else {
|
|
||||||
val uri = Uri.parse("package:${event.activity.packageName}")
|
|
||||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
|
|
||||||
|
|
||||||
intent.launchActivity(event.activity) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!legacyStoragePermission) {
|
|
||||||
val uri = Uri.parse("package:${event.activity.packageName}")
|
|
||||||
val intent = Intent(
|
|
||||||
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
|
|
||||||
uri
|
|
||||||
)
|
|
||||||
|
|
||||||
intent.launchActivity(event.activity) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
storagePermissionJob?.cancel()
|
|
||||||
storagePermissionJob = viewModelScope.launch {
|
|
||||||
while (true) {
|
|
||||||
val granted = if (legacyStoragePermission) {
|
|
||||||
event.storagePermissionState.status.isGranted
|
|
||||||
} else Environment.isExternalStorageManager()
|
|
||||||
|
|
||||||
if (!granted) {
|
|
||||||
delay(1000)
|
|
||||||
yield()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
yield()
|
|
||||||
|
|
||||||
|
if (isPermissionGranted) {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
storagePermissionGranted = true
|
storagePermissionGranted = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
break
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legacyStoragePermission) {
|
||||||
|
if (!event.storagePermissionState.status.shouldShowRationale) {
|
||||||
|
event.storagePermissionState.launchPermissionRequest()
|
||||||
|
} else {
|
||||||
|
val uri = Uri.parse("package:${event.activity.packageName}")
|
||||||
|
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
|
||||||
|
|
||||||
|
intent.launchActivity(event.activity) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!legacyStoragePermission) {
|
||||||
|
val uri = Uri.parse("package:${event.activity.packageName}")
|
||||||
|
val intent = Intent(
|
||||||
|
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
|
||||||
|
uri
|
||||||
|
)
|
||||||
|
|
||||||
|
intent.launchActivity(event.activity) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storagePermissionJob?.cancel()
|
||||||
|
storagePermissionJob = viewModelScope.launch {
|
||||||
|
while (true) {
|
||||||
|
val granted = if (legacyStoragePermission) {
|
||||||
|
event.storagePermissionState.status.isGranted
|
||||||
|
} else Environment.isExternalStorageManager()
|
||||||
|
|
||||||
|
if (!granted) {
|
||||||
|
delay(1000)
|
||||||
|
yield()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
yield()
|
||||||
|
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
storagePermissionGranted = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is StartEvent.OnNotificationsPermissionRequest -> {
|
is StartEvent.OnNotificationsPermissionRequest -> {
|
||||||
if (event.notificationsPermissionState.status.isGranted) {
|
viewModelScope.launch {
|
||||||
_state.update {
|
if (event.notificationsPermissionState.status.isGranted) {
|
||||||
it.copy(
|
|
||||||
notificationsPermissionGranted = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!event.notificationsPermissionState.status.shouldShowRationale) {
|
|
||||||
event.notificationsPermissionState.launchPermissionRequest()
|
|
||||||
} else {
|
|
||||||
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
|
||||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName)
|
|
||||||
|
|
||||||
intent.launchActivity(event.activity) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationsPermissionJob?.cancel()
|
|
||||||
notificationsPermissionJob = viewModelScope.launch {
|
|
||||||
while (true) {
|
|
||||||
if (!event.notificationsPermissionState.status.isGranted) {
|
|
||||||
delay(1000)
|
|
||||||
yield()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
yield()
|
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
notificationsPermissionGranted = true
|
notificationsPermissionGranted = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
break
|
if (!event.notificationsPermissionState.status.shouldShowRationale) {
|
||||||
|
event.notificationsPermissionState.launchPermissionRequest()
|
||||||
|
} else {
|
||||||
|
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
||||||
|
intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName)
|
||||||
|
|
||||||
|
intent.launchActivity(event.activity) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationsPermissionJob?.cancel()
|
||||||
|
notificationsPermissionJob = viewModelScope.launch {
|
||||||
|
while (true) {
|
||||||
|
if (!event.notificationsPermissionState.status.isGranted) {
|
||||||
|
delay(1000)
|
||||||
|
yield()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
yield()
|
||||||
|
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
notificationsPermissionGranted = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,11 +177,19 @@ class StartModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetScreen() {
|
fun resetScreen() {
|
||||||
_state.update {
|
viewModelScope.launch {
|
||||||
storagePermissionJob?.cancel()
|
_state.update {
|
||||||
notificationsPermissionJob?.cancel()
|
storagePermissionJob?.cancel()
|
||||||
|
notificationsPermissionJob?.cancel()
|
||||||
|
|
||||||
StartState()
|
StartState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||||
|
mutex.withLock {
|
||||||
|
this.value = function(this.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue