Skip to content

Commit 3c09bf4

Browse files
committed
refactor: update extractors and error handling
1 parent 09643bb commit 3c09bf4

File tree

17 files changed

+460
-249
lines changed

17 files changed

+460
-249
lines changed

app/src/main/java/com/paulcoding/pindownloader/AppException.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,4 @@ sealed class AppException(messageRes: String) : Exception(messageRes) {
99
class ParseJsonError(url: String) : AppException("Cannot parse JSON for $url")
1010
class MessageError : AppException("Cannot parse url from message")
1111
class UnknownError : AppException("Something went wrong")
12-
class PremiumRequired : AppException("Premium Required")
1312
}

app/src/main/java/com/paulcoding/pindownloader/MainViewModel.kt

Lines changed: 60 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,38 @@
11
package com.paulcoding.pindownloader
22

3+
import android.content.Context
4+
import android.graphics.drawable.BitmapDrawable
35
import androidx.lifecycle.ViewModel
46
import androidx.lifecycle.viewModelScope
7+
import coil3.Bitmap
8+
import coil3.BitmapImage
9+
import coil3.ImageLoader
10+
import coil3.request.ImageRequest
11+
import coil3.request.SuccessResult
12+
import coil3.request.allowHardware
513
import com.paulcoding.pindownloader.App.Companion.appContext
6-
import com.paulcoding.pindownloader.extractor.PinData
714
import com.paulcoding.pindownloader.extractor.PinSource
8-
import com.paulcoding.pindownloader.extractor.PinType
915
import com.paulcoding.pindownloader.extractor.pinterest.PinterestExtractor
1016
import com.paulcoding.pindownloader.extractor.pixiv.PixivExtractor
1117
import com.paulcoding.pindownloader.helper.Downloader
1218
import com.paulcoding.pindownloader.helper.NetworkUtil
19+
import com.paulcoding.pindownloader.ui.model.DownloadInfo
1320
import kotlinx.coroutines.Dispatchers
1421
import kotlinx.coroutines.flow.MutableStateFlow
1522
import kotlinx.coroutines.flow.asStateFlow
1623
import kotlinx.coroutines.flow.update
1724
import kotlinx.coroutines.launch
1825

1926
class MainViewModel(private val downloader: Downloader) : ViewModel() {
20-
private var _uiStateFlow = MutableStateFlow(UiState())
21-
val uiStateFlow = _uiStateFlow.asStateFlow()
27+
private var _state = MutableStateFlow<UiState>(UiState())
28+
val uiStateFlow = _state.asStateFlow()
2229

2330
private val pinterestExtractor = PinterestExtractor()
2431
private val pixivExtractor = PixivExtractor()
2532

26-
data class UiState(
27-
val input: String = "",
28-
val exception: AppException? = null,
29-
val pinData: PinData? = null,
30-
val isFetchingImages: Boolean = false,
31-
val isFetched: Boolean = false,
32-
val isDownloadingImage: Boolean = false,
33-
val isDownloadingVideo: Boolean = false,
34-
val isDownloaded: Boolean = false,
35-
)
3633

3734
private suspend fun extract(link: String) {
38-
_uiStateFlow.update { UiState().copy(input = link) }
35+
_state.update { it.copy(extractState = ExtractState.Loading(link)) }
3936

4037
val extractor =
4138
when {
@@ -45,93 +42,98 @@ class MainViewModel(private val downloader: Downloader) : ViewModel() {
4542
}
4643

4744
if (extractor == null) {
48-
setError(AppException.InvalidUrlError(link))
45+
setExtractError(AppException.InvalidUrlError(link))
4946
return
5047
}
5148

52-
_uiStateFlow.update { it.copy(isFetchingImages = true) }
53-
5449
try {
5550
extractor.extract(link).let { data ->
56-
_uiStateFlow.update { it.copy(pinData = data) }
51+
val bitmap = data.image?.let {
52+
downloadImageBitmap(appContext, it)
53+
}
54+
_state.update { it.copy(extractState = ExtractState.Success(data, bitmap)) }
55+
5756
}
58-
} catch (e: AppException) {
59-
e.printStackTrace()
60-
setError(e)
6157
} catch (e: Exception) {
6258
e.printStackTrace()
63-
setError(AppException.UnknownError())
59+
if (e is AppException) {
60+
setExtractError(e)
61+
}
62+
setExtractError(AppException.UnknownError())
6463
}
65-
66-
_uiStateFlow.update { it.copy(isFetchingImages = false, isFetched = true) }
6764
}
6865

69-
private fun setError(appException: AppException) {
70-
_uiStateFlow.update { it.copy(exception = appException) }
66+
private fun setExtractError(appException: AppException) {
67+
_state.update { it.copy(extractState = ExtractState.Error(appException)) }
7168
}
7269

7370
fun clearPinData() {
74-
_uiStateFlow.update { UiState() }
71+
_state.value = UiState()
72+
}
73+
74+
private suspend fun downloadImageBitmap(context: Context, url: String): Bitmap? {
75+
val loader = ImageLoader(context)
76+
val request = ImageRequest.Builder(context)
77+
.data(url)
78+
.allowHardware(false) // Disable hardware bitmaps if needed
79+
.build()
80+
81+
val result = loader.execute(request)
82+
if (result is SuccessResult) {
83+
return (result.image as BitmapImage).bitmap
84+
}
85+
return null
7586
}
7687

77-
fun download(
78-
link: String,
79-
type: PinType = PinType.IMAGE,
80-
source: PinSource = PinSource.PINTEREST,
81-
fileName: String? = null,
82-
onSuccess: (path: String) -> Unit = {}
83-
) {
88+
fun dispatch(action: MainAction) {
89+
when (action) {
90+
is MainAction.ExtractLink -> extractLink(action.url)
91+
is MainAction.Download -> download(action.downloadInfo)
92+
is MainAction.ClearPinData -> clearPinData()
93+
}
94+
}
95+
96+
private fun download(downloadInfo: DownloadInfo) {
97+
val (link, type, source, fileName) = downloadInfo
98+
8499
val headers =
85100
if (source == PinSource.PIXIV) mapOf("referer" to "https://www.pixiv.net/") else mapOf()
86101

87102
viewModelScope.launch(Dispatchers.IO) {
88-
if (type == PinType.VIDEO) {
89-
_uiStateFlow.update { it.copy(isDownloadingVideo = true) }
90-
} else {
91-
_uiStateFlow.update { it.copy(isDownloadingImage = true) }
92-
}
103+
_state.update { it.copy(downloadState = DownloadState.Loading(link)) }
93104
checkInternetOrExec {
94105
try {
95106
val downloadPath = downloader.download(appContext, link, fileName, headers)
96-
_uiStateFlow.update { it.copy(isDownloadingVideo = false) }
97-
onSuccess(downloadPath)
98-
} catch (e: AppException) {
107+
_state.update { it.copy(downloadState = DownloadState.Success(downloadPath)) }
108+
} catch (e: Exception) {
99109
e.printStackTrace()
100-
setError(e)
101-
}
102-
}
103110

104-
if (type == PinType.VIDEO) {
105-
_uiStateFlow.update { it.copy(isDownloadingVideo = false) }
106-
} else {
107-
_uiStateFlow.update { it.copy(isDownloadingImage = false) }
111+
if (e is AppException)
112+
_state.update { it.copy(downloadState = DownloadState.Error(e)) }
113+
else
114+
_state.update { it.copy(downloadState = DownloadState.Error(AppException.DownloadError(link))) }
115+
}
108116
}
109117
}
110118
}
111119

112-
fun setLink(link: String) {
113-
_uiStateFlow.update { it.copy(input = link) }
114-
}
115-
116120
private suspend fun checkInternetOrExec(block: suspend () -> Unit) {
117121
if (NetworkUtil.isInternetAvailable()) {
118122
return block()
119123
}
120-
return setError(AppException.NetworkError())
124+
return setExtractError(AppException.NetworkError())
121125
}
122126

123127
fun extractLink(msg: String) {
124-
_uiStateFlow.update { UiState().copy(input = msg) }
125128
viewModelScope.launch(Dispatchers.IO) {
126129
checkInternetOrExec {
127130
val urlPattern = """(https?://\S+)""".toRegex()
128131
val link = urlPattern.find(msg)?.value
129132

130133
if (link == null) {
131-
setError(AppException.MessageError())
134+
setExtractError(AppException.MessageError())
132135
return@checkInternetOrExec
133136
}
134-
setLink(link)
135137
extract(link)
136138
}
137139
}
Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
package com.paulcoding.pindownloader
22

3+
import android.annotation.SuppressLint
34
import android.content.Intent
45
import android.os.Bundle
56
import androidx.activity.ComponentActivity
67
import androidx.activity.compose.LocalActivity
78
import androidx.activity.compose.setContent
89
import androidx.activity.enableEdgeToEdge
910
import androidx.compose.foundation.layout.Box
10-
import androidx.compose.foundation.layout.fillMaxWidth
11-
import androidx.compose.foundation.layout.size
11+
import androidx.compose.foundation.layout.fillMaxSize
12+
import androidx.compose.foundation.layout.padding
1213
import androidx.compose.material3.ExperimentalMaterial3Api
13-
import androidx.compose.material3.ModalBottomSheet
14-
import androidx.compose.material3.rememberModalBottomSheetState
14+
import androidx.compose.material3.Scaffold
15+
import androidx.compose.material3.SnackbarHost
16+
import androidx.compose.material3.SnackbarHostState
1517
import androidx.compose.runtime.Composable
16-
import androidx.compose.runtime.LaunchedEffect
1718
import androidx.compose.runtime.getValue
19+
import androidx.compose.runtime.remember
1820
import androidx.compose.ui.Alignment
1921
import androidx.compose.ui.Modifier
20-
import androidx.compose.ui.platform.LocalConfiguration
22+
import androidx.compose.ui.graphics.Color
2123
import androidx.compose.ui.unit.dp
2224
import androidx.lifecycle.compose.collectAsStateWithLifecycle
23-
import com.paulcoding.pindownloader.ui.component.Indicator
25+
import com.paulcoding.pindownloader.component.AppExceptionText
26+
import com.paulcoding.pindownloader.component.DownloadEffect
27+
import com.paulcoding.pindownloader.component.LoadingOverlay
2428
import com.paulcoding.pindownloader.ui.page.home.FetchResult
2529
import org.koin.android.ext.android.inject
2630

@@ -29,11 +33,13 @@ class QuickDownloadActivity : ComponentActivity() {
2933

3034
override fun onCreate(savedInstanceState: Bundle?) {
3135
super.onCreate(savedInstanceState)
32-
3336
handleIntent(intent)
3437
enableEdgeToEdge()
3538
setContent {
36-
DownloadView(viewModel)
39+
val state by viewModel.uiStateFlow.collectAsStateWithLifecycle()
40+
val (extractState, downloadState) = state
41+
42+
DownloadView(extractState, downloadState, onAction = viewModel::dispatch)
3743
}
3844
}
3945

@@ -61,40 +67,39 @@ class QuickDownloadActivity : ComponentActivity() {
6167
}
6268
}
6369

70+
@SuppressLint("ConfigurationScreenWidthHeight")
6471
@OptIn(ExperimentalMaterial3Api::class)
6572
@Composable
66-
fun DownloadView(viewModel: MainViewModel) {
67-
val sheetState = rememberModalBottomSheetState()
68-
val state by viewModel.uiStateFlow.collectAsStateWithLifecycle()
69-
val width = LocalConfiguration.current.screenWidthDp.dp
70-
val context = LocalActivity.current
73+
fun DownloadView(extractState: ExtractState, downloadState: DownloadState, onAction: (MainAction) -> Unit) {
74+
val activity = LocalActivity.current
75+
val snackbarHostState = remember { SnackbarHostState() }
7176

72-
LaunchedEffect(state.isFetched) {
73-
if (state.isFetched) {
74-
sheetState.expand()
75-
}
77+
DownloadEffect(downloadState, snackbarHostState) {
78+
activity?.finish()
7679
}
7780

78-
ModalBottomSheet(
79-
onDismissRequest = { context?.finish() },
80-
sheetState = sheetState,
81-
) {
82-
if (state.isFetchingImages) Box(
81+
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }, containerColor = Color.Transparent) { paddingValues ->
82+
Box(
8383
modifier = Modifier
84-
.align(Alignment.CenterHorizontally)
85-
.size(width - 32.dp)
84+
.fillMaxSize()
85+
.padding(paddingValues)
86+
.padding(bottom = 24.dp),
87+
contentAlignment = Alignment.BottomCenter
8688
) {
87-
Indicator(
88-
modifier = Modifier
89-
.size(64.dp)
90-
.align(Alignment.Center)
91-
)
89+
when (extractState) {
90+
is ExtractState.Error -> {
91+
AppExceptionText(extractState.exception)
92+
}
93+
ExtractState.Idle -> {}
94+
is ExtractState.Loading -> LoadingOverlay()
95+
is ExtractState.Success ->
96+
FetchResult(
97+
pinData = extractState.pinData,
98+
downloadState = downloadState,
99+
showLoadingMaxSize = false,
100+
onAction = onAction,
101+
)
102+
}
92103
}
93-
94-
FetchResult(
95-
modifier = Modifier.fillMaxWidth(),
96-
viewModel = viewModel,
97-
onDownloaded = { context?.finish() }
98-
)
99104
}
100-
}
105+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.paulcoding.pindownloader
2+
3+
import coil3.Bitmap
4+
import com.paulcoding.pindownloader.extractor.PinData
5+
import com.paulcoding.pindownloader.extractor.PinSource
6+
import com.paulcoding.pindownloader.extractor.PinType
7+
import com.paulcoding.pindownloader.ui.model.DownloadInfo
8+
9+
data class UiState(
10+
val extractState: ExtractState = ExtractState.Idle,
11+
val downloadState: DownloadState = DownloadState.Idle,
12+
)
13+
14+
sealed class ExtractState {
15+
data object Idle : ExtractState()
16+
class Loading(val url: String) : ExtractState()
17+
class Success(val pinData: PinData, bitmap: Bitmap?) : ExtractState()
18+
class Error(val exception: AppException) : ExtractState()
19+
}
20+
21+
sealed class DownloadState {
22+
data object Idle : DownloadState()
23+
class Loading(val url: String) : DownloadState()
24+
class Success(val downloadedPath: String) : DownloadState()
25+
class Error(val exception: AppException) : DownloadState()
26+
}
27+
28+
sealed class MainAction {
29+
class ExtractLink(val url: String) : MainAction()
30+
class Download(val downloadInfo: DownloadInfo) : MainAction()
31+
32+
data object ClearPinData : MainAction()
33+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.paulcoding.pindownloader.component
2+
3+
import androidx.compose.material3.MaterialTheme
4+
import androidx.compose.material3.Text
5+
import androidx.compose.runtime.Composable
6+
import com.paulcoding.pindownloader.AppException
7+
import com.paulcoding.pindownloader.util.toMessage
8+
9+
@Composable
10+
fun AppExceptionText(exception: AppException) {
11+
Text(
12+
text = exception.toMessage(),
13+
color = MaterialTheme.colorScheme.error
14+
)
15+
}

0 commit comments

Comments
 (0)