Skip to content

Commit 199aae1

Browse files
committed
Introduce CropSession scoped to crop nav graph and migrate to screen specific ViewModels accessing it
1 parent 0b97e8a commit 199aae1

File tree

15 files changed

+264
-254
lines changed

15 files changed

+264
-254
lines changed

app/src/main/kotlin/com/w2sv/autocrop/MainActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
2121

2222
if (BuildConfig.DEBUG) {
2323
findNavController(R.id.nav_host_fragment).currentBackStack.collectOn(lifecycleScope) { backStackEntries ->
24-
i { "BackStack: ${backStackEntries.map { it.destination.displayName }}" }
24+
i { "BackStack: ${backStackEntries.map { it.destination.displayName.substringAfterLast("/") }}" }
2525
}
2626
}
2727
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.w2sv.autocrop.di
2+
3+
import com.w2sv.domain.session.CropSession
4+
import dagger.Module
5+
import dagger.Provides
6+
import dagger.hilt.InstallIn
7+
import dagger.hilt.android.components.ViewModelComponent
8+
import dagger.hilt.android.scopes.ViewModelScoped
9+
10+
@Module
11+
@InstallIn(ViewModelComponent::class)
12+
object CropSessionModule {
13+
14+
@Provides
15+
@ViewModelScoped
16+
fun provideCropSession(): CropSession =
17+
CropSession()
18+
}

app/src/main/kotlin/com/w2sv/autocrop/ui/screen/CropBundleViewModel.kt

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.w2sv.autocrop.ui.screen
22

33
import android.content.Context
4+
import androidx.annotation.MainThread
45
import androidx.fragment.app.Fragment
6+
import androidx.fragment.app.viewModels
57
import androidx.hilt.navigation.fragment.hiltNavGraphViewModels
68
import androidx.lifecycle.LiveData
79
import androidx.lifecycle.MutableLiveData
@@ -12,10 +14,12 @@ import com.w2sv.autocrop.R
1214
import com.w2sv.autocrop.ui.util.nonNullValue
1315
import com.w2sv.cropping.io.CropBundleIOProcessingUseCase
1416
import com.w2sv.domain.model.CropBundle
15-
import com.w2sv.domain.model.CropResult
17+
import com.w2sv.domain.model.CropBundleProcessingResult
1618
import com.w2sv.domain.model.Screenshot
1719
import com.w2sv.domain.repository.PreferencesRepository
20+
import com.w2sv.domain.session.CropSession
1821
import dagger.hilt.android.lifecycle.HiltViewModel
22+
import dagger.hilt.android.lifecycle.withCreationCallback
1923
import javax.inject.Inject
2024
import kotlinx.coroutines.Dispatchers
2125
import kotlinx.coroutines.Job
@@ -27,6 +31,26 @@ import kotlinx.coroutines.withContext
2731
inline fun <reified VM : ViewModel> Fragment.cropNavGraphViewModel(): Lazy<VM> =
2832
hiltNavGraphViewModels<VM>(R.id.crop_nav_graph)
2933

34+
@HiltViewModel
35+
class CropSessionViewModel @Inject constructor(val cropSession: CropSession) : ViewModel()
36+
37+
interface CropSessionAccessingViewModelFactory<VM : ViewModel> {
38+
fun create(cropSession: CropSession): VM
39+
}
40+
41+
@MainThread
42+
inline fun <reified VM : ViewModel, F : CropSessionAccessingViewModelFactory<VM>> Fragment.cropSessionInjectedViewModel(): Lazy<VM> {
43+
val cropSessionViewModel by cropNavGraphViewModel<CropSessionViewModel>()
44+
return viewModels<VM>(
45+
extrasProducer = {
46+
defaultViewModelCreationExtras
47+
.withCreationCallback<F> { factory ->
48+
factory.create(cropSessionViewModel.cropSession)
49+
}
50+
}
51+
)
52+
}
53+
3054
@HiltViewModel
3155
class CropBundleViewModel @Inject constructor(
3256
private val cropBundleIOProcessingUseCase: CropBundleIOProcessingUseCase,
@@ -52,9 +76,9 @@ class CropBundleViewModel @Inject constructor(
5276

5377
val cropBundleCount: Int get() = cropBundles.size
5478

55-
private val cropBundleIOResults = mutableListOf<CropResult>()
79+
private val cropBundleIOResults = mutableListOf<CropBundleProcessingResult>()
5680

57-
fun deletionApprovalRequiringCropBundleIOResults(): List<CropResult> =
81+
fun deletionApprovalRequiringCropBundleIOResults(): List<CropBundleProcessingResult> =
5882
cropBundleIOResults.filter {
5983
it.screenshotDeletionResult is Screenshot.DeletionResult.ApprovalRequired
6084
}

app/src/main/kotlin/com/w2sv/autocrop/ui/screen/crop/CropScreenFragment.kt renamed to app/src/main/kotlin/com/w2sv/autocrop/ui/screen/crop/CropFragment.kt

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,34 @@ package com.w2sv.autocrop.ui.screen.crop
22

33
import android.os.Bundle
44
import android.view.View
5-
import androidx.fragment.app.viewModels
65
import androidx.lifecycle.lifecycleScope
6+
import com.w2sv.androidutils.BackPressHandler
77
import com.w2sv.androidutils.widget.showToast
88
import com.w2sv.autocrop.databinding.CropBinding
99
import com.w2sv.autocrop.ui.AppFragment
1010
import com.w2sv.autocrop.ui.designsystem.navigateAnimated
11-
import com.w2sv.autocrop.ui.screen.CropBundleViewModel
12-
import com.w2sv.autocrop.ui.screen.cropNavGraphViewModel
13-
import com.w2sv.autocrop.util.launchAfterShortDelay
11+
import com.w2sv.autocrop.ui.screen.cropSessionInjectedViewModel
12+
import com.w2sv.autocrop.ui.util.Constant
1413
import com.w2sv.core.common.R.string as Strings
15-
import com.w2sv.domain.model.CropResults
14+
import com.w2sv.kotlinutils.threadUnsafeLazy
1615
import dagger.hilt.android.AndroidEntryPoint
1716
import kotlinx.coroutines.launch
1817

1918
@AndroidEntryPoint
20-
class CropScreenFragment : AppFragment<CropBinding>(CropBinding::class.java) {
19+
class CropFragment : AppFragment<CropBinding>(CropBinding::class.java) {
2120

22-
private val viewModel by viewModels<CropScreenViewModel>()
23-
private val cropBundleVM by cropNavGraphViewModel<CropBundleViewModel>()
21+
private val viewModel by cropSessionInjectedViewModel<CropViewModel, CropViewModel.Factory>()
22+
23+
private val backPressListener by threadUnsafeLazy {
24+
BackPressHandler(
25+
coroutineScope = lifecycleScope,
26+
confirmationWindowDuration = Constant.BACKPRESS_CONFIRMATION_WINDOW_DURATION
27+
)
28+
}
2429

2530
override val onBackPressed: () -> Unit
2631
get() = {
27-
viewModel.backPressListener(
32+
backPressListener(
2833
onFirstPress = {
2934
requireContext().showToast(getString(Strings.tap_again_to_cancel))
3035
},
@@ -53,20 +58,9 @@ class CropScreenFragment : AppFragment<CropBinding>(CropBinding::class.java) {
5358
lifecycleScope.launch {
5459
viewModel.cropScreenshots(
5560
contentResolver = requireContext().contentResolver,
56-
onCropBundle = cropBundleVM::addCropBundle,
57-
onFinishedListener = ::invokeSubsequentScreen
61+
onAnySuccessfulCrops = { navController.navigateAnimated(CropFragmentDirections.navigateToCropPagerScreen()) },
62+
onNoSuccessfulCrops = { navController.navigateAnimated(CropFragmentDirections.navigateToCroppingFailedScreen()) }
5863
)
5964
}
6065
}
61-
62-
private fun invokeSubsequentScreen(cropResults: CropResults) {
63-
if (cropBundleVM.cropBundles.isNotEmpty()) {
64-
navController.navigateAnimated(CropScreenFragmentDirections.navigateToCropPagerScreen(cropResults))
65-
} else {
66-
launchAfterShortDelay {
67-
// to assure progress bar having reached 100% before UI change
68-
navController.navigateAnimated(CropScreenFragmentDirections.navigateToCroppingFailedScreen())
69-
}
70-
}
71-
}
7266
}

app/src/main/kotlin/com/w2sv/autocrop/ui/screen/crop/CropScreenViewModel.kt

Lines changed: 0 additions & 102 deletions
This file was deleted.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.w2sv.autocrop.ui.screen.crop
2+
3+
import android.content.ContentResolver
4+
import android.net.Uri
5+
import androidx.lifecycle.LiveData
6+
import androidx.lifecycle.MutableLiveData
7+
import androidx.lifecycle.SavedStateHandle
8+
import androidx.lifecycle.ViewModel
9+
import androidx.lifecycle.viewModelScope
10+
import com.w2sv.androidutils.lifecycle.increment
11+
import com.w2sv.autocrop.CropNavGraphArgs
12+
import com.w2sv.autocrop.ui.screen.CropSessionAccessingViewModelFactory
13+
import com.w2sv.autocrop.ui.util.nonNullValue
14+
import com.w2sv.cropping.cropping.createCropBundle
15+
import com.w2sv.domain.model.CropBundle
16+
import com.w2sv.domain.repository.PreferencesRepository
17+
import com.w2sv.domain.session.CropSession
18+
import dagger.assisted.Assisted
19+
import dagger.assisted.AssistedFactory
20+
import dagger.assisted.AssistedInject
21+
import dagger.hilt.android.lifecycle.HiltViewModel
22+
import kotlinx.coroutines.Dispatchers
23+
import kotlinx.coroutines.flow.SharingStarted
24+
import kotlinx.coroutines.withContext
25+
import slimber.log.i
26+
27+
@HiltViewModel(assistedFactory = CropViewModel.Factory::class)
28+
class CropViewModel @AssistedInject constructor(
29+
savedStateHandle: SavedStateHandle,
30+
preferencesRepository: PreferencesRepository,
31+
@Assisted private val cropSession: CropSession
32+
) : ViewModel() {
33+
34+
private val screenshotUris: List<Uri> = CropNavGraphArgs.fromSavedStateHandle(savedStateHandle).imageUris.toList()
35+
val screenshotCount = screenshotUris.size
36+
37+
val cropProgress: LiveData<Int> get() = _cropProgress
38+
private val _cropProgress = MutableLiveData(0)
39+
40+
private val imminentUris: List<Uri>
41+
get() = screenshotUris.run {
42+
subList(cropProgress.nonNullValue, size)
43+
}
44+
45+
private val cropSensitivity = preferencesRepository.cropSensitivity.stateIn(viewModelScope, SharingStarted.Eagerly)
46+
47+
suspend fun cropScreenshots(
48+
contentResolver: ContentResolver,
49+
onAnySuccessfulCrops: () -> Unit,
50+
onNoSuccessfulCrops: () -> Unit
51+
) {
52+
imminentUris.forEach { uri ->
53+
withContext(Dispatchers.IO) {
54+
attemptCropBundleCreation(uri, contentResolver)
55+
}
56+
_cropProgress.increment()
57+
}
58+
59+
if (cropSession.bundles.isNotEmpty()) {
60+
onAnySuccessfulCrops()
61+
} else {
62+
onNoSuccessfulCrops()
63+
}
64+
}
65+
66+
private fun attemptCropBundleCreation(screenshotUri: Uri, contentResolver: ContentResolver) {
67+
i { "attemptCropBundleCreation; screenshotUri=$screenshotUri" }
68+
69+
return createCropBundle(
70+
screenshotMediaUri = screenshotUri,
71+
cropSensitivity = cropSensitivity.value,
72+
contentResolver = contentResolver
73+
)
74+
.run {
75+
when (this) {
76+
is CropBundle.CreationResult.NoCropEdgesFound -> cropSession.addUncroppableImage(screenshotUri)
77+
is CropBundle.CreationResult.BitmapLoadingFailed -> cropSession.addUnopenableImage(screenshotUri)
78+
is CropBundle.CreationResult.Success -> cropSession.add(cropBundle)
79+
}
80+
}
81+
}
82+
83+
@AssistedFactory
84+
interface Factory : CropSessionAccessingViewModelFactory<CropViewModel>
85+
}

app/src/main/kotlin/com/w2sv/autocrop/ui/screen/pager/CropPagerScreenFragment.kt renamed to app/src/main/kotlin/com/w2sv/autocrop/ui/screen/pager/CropInspectionFragment.kt

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ import androidx.compose.ui.unit.dp
4040
import androidx.core.net.toUri
4141
import androidx.lifecycle.compose.collectAsStateWithLifecycle
4242
import com.w2sv.autocrop.R
43-
import com.w2sv.autocrop.ui.screen.CropBundleViewModel
44-
import com.w2sv.autocrop.ui.screen.cropNavGraphViewModel
43+
import com.w2sv.autocrop.ui.screen.cropSessionInjectedViewModel
4544
import com.w2sv.autocrop.ui.screen.pager.dialogs.ProcessCropBundleDialog
4645
import com.w2sv.autocrop.ui.theme.AppTheme
4746
import com.w2sv.autocrop.util.ComposeFragment
@@ -56,27 +55,27 @@ import kotlinx.collections.immutable.persistentListOf
5655
import kotlinx.collections.immutable.toImmutableList
5756

5857
@AndroidEntryPoint
59-
class CropPagerScreenFragment : ComposeFragment() {
58+
class CropInspectionFragment : ComposeFragment() {
6059

61-
private val cropBundleVM by cropNavGraphViewModel<CropBundleViewModel>()
60+
private val viewModel by cropSessionInjectedViewModel<CropInspectionViewModel, CropInspectionViewModel.Factory>()
6261

6362
@Composable
6463
override fun ScreenContent() {
6564
val context = LocalContext.current
66-
val deleteScreenshots by cropBundleVM.deleteScreenshots.collectAsStateWithLifecycle()
65+
val deleteScreenshots by viewModel.deleteScreenshots.collectAsStateWithLifecycle()
6766

6867
CropPagerScreen(
69-
cropBundles = cropBundleVM.cropBundles.toImmutableList(),
70-
discardCropBundleAt = { cropBundleVM.discardCropBundleAt(it) },
71-
processCropBundleAt = { cropBundleVM.processCropBundleAt(it, context) },
68+
cropBundles = viewModel.cropBundles.toImmutableList(),
69+
discardCropBundleAt = { viewModel.discardCropBundleAt(it) },
70+
processCropBundleAt = { viewModel.processCropBundleAt(it, context) },
7271
deleteScreenshots = { deleteScreenshots },
73-
toggleDeleteScreenshots = { cropBundleVM.toggleDeleteScreenshots() }
72+
toggleDeleteScreenshots = { viewModel.toggleDeleteScreenshots() }
7473
)
7574
}
7675
}
7776

7877
@Composable
79-
fun CropPagerScreen(
78+
private fun CropPagerScreen(
8079
cropBundles: ImmutableList<CropBundle>,
8180
discardCropBundleAt: (Int) -> Unit,
8281
processCropBundleAt: (Int) -> Unit,

0 commit comments

Comments
 (0)