@@ -55,6 +55,10 @@ class FilteredDeckOptionsViewModel(
5555
5656 private var decksNames = emptyList<String >()
5757
58+ val hasUnsavedChanges: StateFlow <Boolean >
59+ field = MutableStateFlow <Boolean >(false )
60+ private var initialState: FilteredDeckOptions ? = null
61+
5862 /* * The [DeckId] of a filtered deck to edit or 0 if we are creating a new filtered deck. */
5963 private val did: DeckId
6064 get() = savedStateHandle.get<Long >(ARG_DECK_ID ) ? : 0L
@@ -79,12 +83,15 @@ class FilteredDeckOptionsViewModel(
7983 return @launch
8084 }
8185 decksNames = withCol { safeGetDecksNames() }
82- savedStateHandle[ ARG_DATA ] =
83- filteredDeckData .asInitialState(
86+ filteredDeckData
87+ .asInitialState(
8488 cardsOptions = cardsOptions,
8589 defaultSearch1 = search,
8690 defaultSearch2 = search2,
87- )
91+ ).apply {
92+ savedStateHandle[ARG_DATA ] = this
93+ initialState = this
94+ }
8895 state.update { currentState() }
8996 }
9097 }
@@ -99,6 +106,7 @@ class FilteredDeckOptionsViewModel(
99106 }
100107 if (currentState().name == name) return
101108 updateCurrentState { copy(name = name, nameInputError = error) }
109+ hasUnsavedChanges.update { wasStateModified() }
102110 }
103111
104112 fun onSearchChange (
@@ -109,6 +117,7 @@ class FilteredDeckOptionsViewModel(
109117 val targetFilterState = currentFilterState(index)
110118 if (targetFilterState?.search == searchQuery) return
111119 updateCurrentFilterState(index) { copy(search = searchQuery) }
120+ hasUnsavedChanges.update { wasStateModified() }
112121 }
113122
114123 fun onLimitChange (
@@ -124,6 +133,7 @@ class FilteredDeckOptionsViewModel(
124133 }
125134 if (limit == currentFilterState(index)?.limit) return
126135 updateCurrentFilterState(index) { copy(limit = limit, error = inputError) }
136+ hasUnsavedChanges.update { wasStateModified() }
127137 }
128138
129139 fun onCardsOptionsChange (
@@ -133,6 +143,7 @@ class FilteredDeckOptionsViewModel(
133143 Timber .i(" Filtered deck filter($filterIndex ) cards options index changing to $cardOptionIndex " )
134144 if (currentFilterState(filterIndex)?.index == cardOptionIndex) return
135145 updateCurrentFilterState(filterIndex) { copy(index = cardOptionIndex) }
146+ hasUnsavedChanges.update { wasStateModified() }
136147 }
137148
138149 fun onSearchInBrowser (filterIndex : FilterIndex ) {
@@ -170,6 +181,7 @@ class FilteredDeckOptionsViewModel(
170181 filter2State = filter2State ? : SearchTermState (),
171182 )
172183 }
184+ hasUnsavedChanges.update { wasStateModified() }
173185 }
174186
175187 fun onRescheduleChange (isEnabled : Boolean ) {
@@ -214,7 +226,8 @@ class FilteredDeckOptionsViewModel(
214226 fun onShowExcludedCards () {
215227 Timber .i(" Building unmovable cards search query to show in browser" )
216228 viewModelScope.launch {
217- val manualFilters = mutableListOf (currentFilterState(FilterIndex .First )?.search ? : return @launch)
229+ val manualFilters =
230+ mutableListOf (currentFilterState(FilterIndex .First )?.search ? : return @launch)
218231 if (currentState().isSecondFilterEnabled && currentFilterState(FilterIndex .Second ) != null ) {
219232 manualFilters.add(currentFilterState(FilterIndex .Second )?.search ? : return @launch)
220233 }
@@ -227,7 +240,8 @@ class FilteredDeckOptionsViewModel(
227240 val manualFilter = withCol { groupSearches(manualFilters, SearchJoiner .OR ) }
228241 val implicitFilter = withCol { groupSearches(implicitFilters, SearchJoiner .OR ) }
229242 try {
230- val browserSearch = withCol { buildSearchString(listOf (manualFilter, implicitFilter)) }
243+ val browserSearch =
244+ withCol { buildSearchString(listOf (manualFilter, implicitFilter)) }
231245 updateCurrentState { copy(browserQuery = browserSearch) }
232246 } catch (ex: Exception ) {
233247 updateCurrentState { copy(throwable = ex) }
@@ -280,6 +294,38 @@ class FilteredDeckOptionsViewModel(
280294 updateCurrentState { copy(throwable = null ) }
281295 }
282296
297+ /* *
298+ * Checks the current state with the state that was loaded initially.
299+ */
300+ private fun wasStateModified (): Boolean {
301+ val initial = initialState ? : return false
302+ val current = (state.value as ? FilteredDeckOptions ) ? : return false
303+ return initial.name != current.name ||
304+ isFilter1Changed(initial, current) ||
305+ initial.isSecondFilterEnabled != current.isSecondFilterEnabled ||
306+ isFilter2Changed(initial, current)
307+ }
308+
309+ private fun isFilter1Changed (
310+ initial : FilteredDeckOptions ,
311+ current : FilteredDeckOptions ,
312+ ): Boolean =
313+ initial.filter1State.search != current.filter1State.search ||
314+ initial.filter1State.limit != current.filter1State.limit ||
315+ initial.filter1State.index != current.filter1State.index
316+
317+ private fun isFilter2Changed (
318+ initial : FilteredDeckOptions ,
319+ current : FilteredDeckOptions ,
320+ ): Boolean =
321+ if (! current.isSecondFilterEnabled || ! initial.isSecondFilterEnabled) {
322+ false
323+ } else {
324+ initial.filter2State?.search != current.filter2State?.search ||
325+ initial.filter2State?.limit != current.filter2State?.limit ||
326+ initial.filter2State?.index != current.filter2State?.index
327+ }
328+
283329 /* * Get the current state as it's found in the associated [SavedStateHandle]. Throws if state is not found. */
284330 private fun currentState (): FilteredDeckOptions = requireNotNull(savedStateHandle[ARG_DATA ])
285331
0 commit comments