Add download APK file
This commit is contained in:
		
							parent
							
								
									aa6517818c
								
							
						
					
					
						commit
						840970bfe2
					
				| @ -21,8 +21,8 @@ android { | |||||||
|         applicationId "me.ash.reader" |         applicationId "me.ash.reader" | ||||||
|         minSdk 26 |         minSdk 26 | ||||||
|         targetSdk 32 |         targetSdk 32 | ||||||
|         versionCode 5 |         versionCode 6 | ||||||
|         versionName "0.7.2" |         versionName "0.7.4" | ||||||
| 
 | 
 | ||||||
|         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" |         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||||||
|         vectorDrawables { |         vectorDrawables { | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ | |||||||
|     package="me.ash.reader"> |     package="me.ash.reader"> | ||||||
| 
 | 
 | ||||||
|     <uses-permission android:name="android.permission.INTERNET" /> |     <uses-permission android:name="android.permission.INTERNET" /> | ||||||
|  |     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> | ||||||
| 
 | 
 | ||||||
|     <application |     <application | ||||||
|         android:name=".App" |         android:name=".App" | ||||||
| @ -24,10 +25,21 @@ | |||||||
|                 <category android:name="android.intent.category.LAUNCHER" /> |                 <category android:name="android.intent.category.LAUNCHER" /> | ||||||
|             </intent-filter> |             </intent-filter> | ||||||
|         </activity> |         </activity> | ||||||
|  | 
 | ||||||
|         <provider |         <provider | ||||||
|             android:name="androidx.startup.InitializationProvider" |             android:name="androidx.startup.InitializationProvider" | ||||||
|             android:authorities="${applicationId}.androidx-startup" |             android:authorities="${applicationId}.androidx-startup" | ||||||
|             tools:node="remove"></provider> |             tools:node="remove" /> | ||||||
|  | 
 | ||||||
|  |         <provider | ||||||
|  |             android:name="androidx.core.content.FileProvider" | ||||||
|  |             android:authorities="${applicationId}.fileprovider" | ||||||
|  |             android:exported="false" | ||||||
|  |             android:grantUriPermissions="true"> | ||||||
|  |             <meta-data | ||||||
|  |                 android:name="android.support.FILE_PROVIDER_PATHS" | ||||||
|  |                 android:resource="@xml/file_paths" /> | ||||||
|  |         </provider> | ||||||
|     </application> |     </application> | ||||||
| 
 | 
 | ||||||
| </manifest> | </manifest> | ||||||
| @ -15,9 +15,7 @@ import me.ash.reader.data.source.AppNetworkDataSource | |||||||
| import me.ash.reader.data.source.OpmlLocalDataSource | import me.ash.reader.data.source.OpmlLocalDataSource | ||||||
| import me.ash.reader.data.source.ReaderDatabase | import me.ash.reader.data.source.ReaderDatabase | ||||||
| import me.ash.reader.data.source.RssNetworkDataSource | import me.ash.reader.data.source.RssNetworkDataSource | ||||||
| import me.ash.reader.ui.ext.DataStoreKeys | import me.ash.reader.ui.ext.* | ||||||
| import me.ash.reader.ui.ext.dataStore |  | ||||||
| import me.ash.reader.ui.ext.put |  | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| @HiltAndroidApp | @HiltAndroidApp | ||||||
| @ -99,7 +97,12 @@ class App : Application(), Configuration.Provider { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private suspend fun checkUpdate() { |     private suspend fun checkUpdate() { | ||||||
|         appRepository.checkUpdate() |         applicationContext.getLatestApk().let { | ||||||
|  |             if (it.exists()) { | ||||||
|  |                 it.del() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         appRepository.checkUpdate(showToast = false) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun getWorkManagerConfiguration(): Configuration = |     override fun getWorkManagerConfiguration(): Configuration = | ||||||
|  | |||||||
| @ -5,12 +5,17 @@ import android.util.Log | |||||||
| import dagger.hilt.android.qualifiers.ApplicationContext | import dagger.hilt.android.qualifiers.ApplicationContext | ||||||
| import kotlinx.coroutines.CoroutineDispatcher | import kotlinx.coroutines.CoroutineDispatcher | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.emptyFlow | ||||||
| import kotlinx.coroutines.withContext | import kotlinx.coroutines.withContext | ||||||
| import me.ash.reader.R | import me.ash.reader.R | ||||||
| import me.ash.reader.data.entity.toVersion | import me.ash.reader.data.entity.toVersion | ||||||
| import me.ash.reader.data.module.ApplicationScope | import me.ash.reader.data.module.ApplicationScope | ||||||
| import me.ash.reader.data.module.DispatcherIO | import me.ash.reader.data.module.DispatcherIO | ||||||
|  | import me.ash.reader.data.module.DispatcherMain | ||||||
| import me.ash.reader.data.source.AppNetworkDataSource | import me.ash.reader.data.source.AppNetworkDataSource | ||||||
|  | import me.ash.reader.data.source.Download | ||||||
|  | import me.ash.reader.data.source.downloadToFileWithProgress | ||||||
| import me.ash.reader.ui.ext.* | import me.ash.reader.ui.ext.* | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| @ -22,11 +27,28 @@ class AppRepository @Inject constructor( | |||||||
|     private val applicationScope: CoroutineScope, |     private val applicationScope: CoroutineScope, | ||||||
|     @DispatcherIO |     @DispatcherIO | ||||||
|     private val dispatcherIO: CoroutineDispatcher, |     private val dispatcherIO: CoroutineDispatcher, | ||||||
|  |     @DispatcherMain | ||||||
|  |     private val dispatcherMain: CoroutineDispatcher, | ||||||
| ) { | ) { | ||||||
|     suspend fun checkUpdate(): Boolean = withContext(dispatcherIO) { |     suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(dispatcherIO) { | ||||||
|         try { |         try { | ||||||
|             val latest = |             val response = | ||||||
|                 appNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link)) |                 appNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link)) | ||||||
|  |             when { | ||||||
|  |                 response.code() == 403 -> { | ||||||
|  |                     withContext(dispatcherMain) { | ||||||
|  |                         if (showToast) context.showToast(context.getString(R.string.rate_limit)) | ||||||
|  |                     } | ||||||
|  |                     return@withContext null | ||||||
|  |                 } | ||||||
|  |                 response.body() == null -> { | ||||||
|  |                     withContext(dispatcherMain) { | ||||||
|  |                         if (showToast) context.showToast(context.getString(R.string.check_failure)) | ||||||
|  |                     } | ||||||
|  |                     return@withContext null | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             val latest = response.body()!! | ||||||
|             val latestVersion = latest.tag_name.toVersion() |             val latestVersion = latest.tag_name.toVersion() | ||||||
| //            val latestVersion = "0.7.3".toVersion() | //            val latestVersion = "0.7.3".toVersion() | ||||||
|             val skipVersion = context.skipVersionNumber.toVersion() |             val skipVersion = context.skipVersionNumber.toVersion() | ||||||
| @ -60,7 +82,26 @@ class AppRepository @Inject constructor( | |||||||
|         } catch (e: Exception) { |         } catch (e: Exception) { | ||||||
|             e.printStackTrace() |             e.printStackTrace() | ||||||
|             Log.e("RLog", "checkUpdate: ${e.message}") |             Log.e("RLog", "checkUpdate: ${e.message}") | ||||||
|             false |             withContext(dispatcherMain) { | ||||||
|  |                 if (showToast) context.showToast(context.getString(R.string.check_failure)) | ||||||
|             } |             } | ||||||
|  |             null | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     suspend fun downloadFile(url: String): Flow<Download> = | ||||||
|  |         withContext(dispatcherIO) { | ||||||
|  |             Log.i("RLog", "downloadFile start: $url") | ||||||
|  |             try { | ||||||
|  |                 return@withContext appNetworkDataSource.downloadFile(url) | ||||||
|  |                     .downloadToFileWithProgress(context.getLatestApk()) | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 e.printStackTrace() | ||||||
|  |                 Log.e("RLog", "downloadFile: ${e.message}") | ||||||
|  |                 withContext(dispatcherMain) { | ||||||
|  |                     context.showToast(context.getString(R.string.download_failure)) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             emptyFlow() | ||||||
|         } |         } | ||||||
| } | } | ||||||
| @ -1,14 +1,33 @@ | |||||||
| package me.ash.reader.data.source | package me.ash.reader.data.source | ||||||
| 
 | 
 | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.distinctUntilChanged | ||||||
|  | import kotlinx.coroutines.flow.flow | ||||||
|  | import kotlinx.coroutines.flow.flowOn | ||||||
| import me.ash.reader.data.entity.LatestRelease | import me.ash.reader.data.entity.LatestRelease | ||||||
|  | import okhttp3.ResponseBody | ||||||
|  | import retrofit2.Response | ||||||
| import retrofit2.Retrofit | import retrofit2.Retrofit | ||||||
| import retrofit2.converter.gson.GsonConverterFactory | import retrofit2.converter.gson.GsonConverterFactory | ||||||
| import retrofit2.http.GET | import retrofit2.http.GET | ||||||
|  | import retrofit2.http.Streaming | ||||||
| import retrofit2.http.Url | import retrofit2.http.Url | ||||||
|  | import java.io.File | ||||||
|  | 
 | ||||||
|  | sealed class Download { | ||||||
|  |     object NotYet : Download() | ||||||
|  |     data class Progress(val percent: Int) : Download() | ||||||
|  |     data class Finished(val file: File) : Download() | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| interface AppNetworkDataSource { | interface AppNetworkDataSource { | ||||||
|     @GET |     @GET | ||||||
|     suspend fun getReleaseLatest(@Url url: String): LatestRelease |     suspend fun getReleaseLatest(@Url url: String): Response<LatestRelease> | ||||||
|  | 
 | ||||||
|  |     @GET | ||||||
|  |     @Streaming | ||||||
|  |     suspend fun downloadFile(@Url url: String): ResponseBody | ||||||
| 
 | 
 | ||||||
|     companion object { |     companion object { | ||||||
|         private var instance: AppNetworkDataSource? = null |         private var instance: AppNetworkDataSource? = null | ||||||
| @ -25,3 +44,52 @@ interface AppNetworkDataSource { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | fun ResponseBody.downloadToFileWithProgress(saveFile: File): Flow<Download> = | ||||||
|  |     flow { | ||||||
|  |         emit(Download.Progress(0)) | ||||||
|  | 
 | ||||||
|  |         // flag to delete file if download errors or is cancelled | ||||||
|  |         var deleteFile = true | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             byteStream().use { inputStream -> | ||||||
|  |                 saveFile.outputStream().use { outputStream -> | ||||||
|  |                     val totalBytes = contentLength() | ||||||
|  |                     val data = ByteArray(8_192) | ||||||
|  |                     var progressBytes = 0L | ||||||
|  | 
 | ||||||
|  |                     while (true) { | ||||||
|  |                         val bytes = inputStream.read(data) | ||||||
|  | 
 | ||||||
|  |                         if (bytes == -1) { | ||||||
|  |                             break | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         outputStream.channel | ||||||
|  |                         outputStream.write(data, 0, bytes) | ||||||
|  |                         progressBytes += bytes | ||||||
|  | 
 | ||||||
|  |                         emit(Download.Progress(percent = ((progressBytes * 100) / totalBytes).toInt())) | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     when { | ||||||
|  |                         progressBytes < totalBytes -> | ||||||
|  |                             throw Exception("missing bytes") | ||||||
|  |                         progressBytes > totalBytes -> | ||||||
|  |                             throw Exception("too many bytes") | ||||||
|  |                         else -> | ||||||
|  |                             deleteFile = false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             emit(Download.Finished(saveFile)) | ||||||
|  |         } finally { | ||||||
|  |             // check if download was successful | ||||||
|  | 
 | ||||||
|  |             if (deleteFile) { | ||||||
|  |                 saveFile.delete() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }.flowOn(Dispatchers.IO).distinctUntilChanged() | ||||||
| @ -3,9 +3,13 @@ package me.ash.reader.ui.ext | |||||||
| import android.app.Activity | import android.app.Activity | ||||||
| import android.content.Context | import android.content.Context | ||||||
| import android.content.ContextWrapper | import android.content.ContextWrapper | ||||||
|  | import android.content.Intent | ||||||
|  | import android.util.Log | ||||||
| import android.widget.Toast | import android.widget.Toast | ||||||
|  | import androidx.core.content.FileProvider | ||||||
| import me.ash.reader.data.entity.Version | import me.ash.reader.data.entity.Version | ||||||
| import me.ash.reader.data.entity.toVersion | import me.ash.reader.data.entity.toVersion | ||||||
|  | import java.io.File | ||||||
| 
 | 
 | ||||||
| fun Context.findActivity(): Activity? = when (this) { | fun Context.findActivity(): Activity? = when (this) { | ||||||
|     is Activity -> this |     is Activity -> this | ||||||
| @ -18,6 +22,27 @@ fun Context.getCurrentVersion(): Version = packageManager | |||||||
|     .versionName |     .versionName | ||||||
|     .toVersion() |     .toVersion() | ||||||
| 
 | 
 | ||||||
|  | fun Context.getLatestApk(): File = File(cacheDir, "latest.apk") | ||||||
|  | 
 | ||||||
|  | fun Context.getFileProvider(): String = "${packageName}.fileprovider" | ||||||
|  | 
 | ||||||
|  | fun Context.installLatestApk() { | ||||||
|  |     try { | ||||||
|  |         val contentUri = FileProvider.getUriForFile(this, getFileProvider(), getLatestApk()) | ||||||
|  |         val intent = Intent(Intent.ACTION_VIEW).apply { | ||||||
|  |             addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||||||
|  |             addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); | ||||||
|  |             setDataAndType(contentUri, "application/vnd.android.package-archive") | ||||||
|  |         } | ||||||
|  |         if (packageManager.queryIntentActivities(intent, 0).size > 0) { | ||||||
|  |             startActivity(intent) | ||||||
|  |         } | ||||||
|  |     } catch (e: Throwable) { | ||||||
|  |         e.printStackTrace() | ||||||
|  |         Log.e("RLog", "installLatestApk: ${e.message}") | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| private var toast: Toast? = null | private var toast: Toast? = null | ||||||
| 
 | 
 | ||||||
| fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) { | fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) { | ||||||
|  | |||||||
							
								
								
									
										38
									
								
								app/src/main/java/me/ash/reader/ui/ext/FileEXT.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								app/src/main/java/me/ash/reader/ui/ext/FileEXT.kt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | |||||||
|  | package me.ash.reader.ui.ext | ||||||
|  | 
 | ||||||
|  | import java.io.BufferedReader | ||||||
|  | import java.io.File | ||||||
|  | import java.io.InputStream | ||||||
|  | import java.io.InputStreamReader | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Convert [InputStream] to [String]. | ||||||
|  |  */ | ||||||
|  | fun InputStream.readString(): String = | ||||||
|  |     BufferedReader(InputStreamReader(this)).useLines { lines -> | ||||||
|  |         val results = StringBuilder() | ||||||
|  |         lines.forEach { results.append(it) } | ||||||
|  |         results.toString() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Delete a file or directory. | ||||||
|  |  */ | ||||||
|  | fun File.del() { | ||||||
|  |     if (this.isFile) { | ||||||
|  |         delete() | ||||||
|  |         return | ||||||
|  |     } | ||||||
|  |     this.listFiles()?.forEach { it.del() } | ||||||
|  |     delete() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun File.mkDir() { | ||||||
|  |     val dirArray = this.absolutePath.split("/".toRegex()) | ||||||
|  |     var pathTemp = "" | ||||||
|  |     for (i in 1 until dirArray.size) { | ||||||
|  |         pathTemp = "$pathTemp/${dirArray[i]}" | ||||||
|  |         val newF = File("${dirArray[0]}$pathTemp") | ||||||
|  |         if (!newF.exists()) newF.mkdir() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -15,6 +15,7 @@ import androidx.compose.ui.platform.LocalContext | |||||||
| import androidx.compose.ui.res.stringResource | import androidx.compose.ui.res.stringResource | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import androidx.compose.ui.zIndex | import androidx.compose.ui.zIndex | ||||||
|  | import androidx.hilt.navigation.compose.hiltViewModel | ||||||
| import androidx.navigation.NavHostController | import androidx.navigation.NavHostController | ||||||
| import kotlinx.coroutines.flow.map | import kotlinx.coroutines.flow.map | ||||||
| import me.ash.reader.R | import me.ash.reader.R | ||||||
| @ -33,9 +34,9 @@ import me.ash.reader.ui.theme.palette.onLight | |||||||
| @Composable | @Composable | ||||||
| fun SettingsPage( | fun SettingsPage( | ||||||
|     navController: NavHostController, |     navController: NavHostController, | ||||||
|  |     updateViewModel: UpdateViewModel = hiltViewModel(), | ||||||
| ) { | ) { | ||||||
|     val context = LocalContext.current |     val context = LocalContext.current | ||||||
|     var updateDialogVisible by remember { mutableStateOf(false) } |  | ||||||
|     val skipVersion = context.dataStore.data |     val skipVersion = context.dataStore.data | ||||||
|         .map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" } |         .map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" } | ||||||
|         .collectAsState(initial = "") |         .collectAsState(initial = "") | ||||||
| @ -92,7 +93,9 @@ fun SettingsPage( | |||||||
|                                         contentDescription = stringResource(R.string.close), |                                         contentDescription = stringResource(R.string.close), | ||||||
|                                     ) |                                     ) | ||||||
|                                 }, |                                 }, | ||||||
|                             ) { updateDialogVisible = true } |                             ) { | ||||||
|  |                                 updateViewModel.dispatch(UpdateViewAction.Show) | ||||||
|  |                             } | ||||||
|                         } |                         } | ||||||
|                         Banner( |                         Banner( | ||||||
|                             title = stringResource(R.string.in_coding), |                             title = stringResource(R.string.in_coding), | ||||||
| @ -148,8 +151,5 @@ fun SettingsPage( | |||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     UpdateDialog( |     UpdateDialog() | ||||||
|         visible = updateDialogVisible, |  | ||||||
|         onDismissRequest = { updateDialogVisible = false }, |  | ||||||
|     ) |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ import androidx.compose.foundation.shape.CircleShape | |||||||
| import androidx.compose.material.icons.Icons | import androidx.compose.material.icons.Icons | ||||||
| import androidx.compose.material.icons.rounded.ArrowBack | import androidx.compose.material.icons.rounded.ArrowBack | ||||||
| import androidx.compose.material.icons.rounded.Balance | import androidx.compose.material.icons.rounded.Balance | ||||||
|  | import androidx.compose.material.icons.rounded.TipsAndUpdates | ||||||
| import androidx.compose.material.icons.rounded.VolunteerActivism | import androidx.compose.material.icons.rounded.VolunteerActivism | ||||||
| import androidx.compose.material3.* | import androidx.compose.material3.* | ||||||
| import androidx.compose.runtime.* | import androidx.compose.runtime.* | ||||||
| @ -30,6 +31,7 @@ import androidx.compose.ui.platform.LocalContext | |||||||
| import androidx.compose.ui.platform.LocalView | import androidx.compose.ui.platform.LocalView | ||||||
| import androidx.compose.ui.res.painterResource | import androidx.compose.ui.res.painterResource | ||||||
| import androidx.compose.ui.res.stringResource | import androidx.compose.ui.res.stringResource | ||||||
|  | import androidx.compose.ui.unit.Dp | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import androidx.hilt.navigation.compose.hiltViewModel | import androidx.hilt.navigation.compose.hiltViewModel | ||||||
| import androidx.navigation.NavHostController | import androidx.navigation.NavHostController | ||||||
| @ -50,13 +52,8 @@ fun TipsAndSupport( | |||||||
|     val context = LocalContext.current |     val context = LocalContext.current | ||||||
|     val view = LocalView.current |     val view = LocalView.current | ||||||
|     val scope = rememberCoroutineScope() |     val scope = rememberCoroutineScope() | ||||||
|     val viewState = updateViewModel.viewState.collectAsStateValue() |  | ||||||
|     val githubLink = stringResource(R.string.github_link) |  | ||||||
|     val telegramLink = stringResource(R.string.telegram_link) |  | ||||||
|     val checkingUpdates = stringResource(R.string.checking_updates) |  | ||||||
|     val isLatestVersion = stringResource(R.string.is_latest_version) |  | ||||||
|     val comingSoon = stringResource(R.string.coming_soon) |  | ||||||
|     var currentVersion by remember { mutableStateOf("") } |     var currentVersion by remember { mutableStateOf("") } | ||||||
|  |     var clickTime by remember { mutableStateOf(System.currentTimeMillis() - 2000) } | ||||||
|     var pressAMP by remember { mutableStateOf(16f) } |     var pressAMP by remember { mutableStateOf(16f) } | ||||||
|     val animatedPress by animateFloatAsState( |     val animatedPress by animateFloatAsState( | ||||||
|         targetValue = pressAMP, |         targetValue = pressAMP, | ||||||
| @ -88,7 +85,16 @@ fun TipsAndSupport( | |||||||
|                         navController.popBackStack() |                         navController.popBackStack() | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 actions = {} |                 actions = { | ||||||
|  |                     FeedbackIconButton( | ||||||
|  |                         modifier = Modifier.size(20.dp), | ||||||
|  |                         imageVector = Icons.Rounded.Balance, | ||||||
|  |                         contentDescription = stringResource(R.string.open_source_licenses), | ||||||
|  |                         tint = MaterialTheme.colorScheme.onSurface | ||||||
|  |                     ) { | ||||||
|  |                         context.showToast(context.getString(R.string.coming_soon)) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|             ) |             ) | ||||||
|         }, |         }, | ||||||
|         content = { |         content = { | ||||||
| @ -110,7 +116,9 @@ fun TipsAndSupport( | |||||||
|                                     pressAMP = 16f |                                     pressAMP = 16f | ||||||
|                                 }, |                                 }, | ||||||
|                                 onTap = { |                                 onTap = { | ||||||
|                                     context.showToast(checkingUpdates) |                                     if (System.currentTimeMillis() - clickTime > 2000) { | ||||||
|  |                                         clickTime = System.currentTimeMillis() | ||||||
|  |                                         context.showToast(context.getString(R.string.checking_updates)) | ||||||
|                                         scope.launch { |                                         scope.launch { | ||||||
|                                             updateViewModel.dispatch( |                                             updateViewModel.dispatch( | ||||||
|                                                 UpdateViewAction.CheckUpdate( |                                                 UpdateViewAction.CheckUpdate( | ||||||
| @ -121,11 +129,18 @@ fun TipsAndSupport( | |||||||
|                                                         ) |                                                         ) | ||||||
|                                                     }, |                                                     }, | ||||||
|                                                     { |                                                     { | ||||||
|                                                     if (!it) context.showToast(isLatestVersion) |                                                         if (!it) { | ||||||
|  |                                                             context.showToast( | ||||||
|  |                                                                 context.getString(R.string.is_latest_version) | ||||||
|  |                                                             ) | ||||||
|  |                                                         } | ||||||
|                                                     } |                                                     } | ||||||
|                                                 ) |                                                 ) | ||||||
|                                             ) |                                             ) | ||||||
|                                         } |                                         } | ||||||
|  |                                     } else { | ||||||
|  |                                         clickTime = System.currentTimeMillis() | ||||||
|  |                                     } | ||||||
|                                 } |                                 } | ||||||
|                             ) |                             ) | ||||||
|                         }, |                         }, | ||||||
| @ -186,22 +201,7 @@ fun TipsAndSupport( | |||||||
|                         ) { |                         ) { | ||||||
|                             view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) |                             view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) | ||||||
|                             view.playSoundEffect(SoundEffectConstants.CLICK) |                             view.playSoundEffect(SoundEffectConstants.CLICK) | ||||||
|                             context.showToast(comingSoon) |                             context.showToast(context.getString(R.string.coming_soon)) | ||||||
|                         }) |  | ||||||
|                         Spacer(modifier = Modifier.width(16.dp)) |  | ||||||
| 
 |  | ||||||
|                         // Telegram |  | ||||||
|                         RoundIconButton(RoundIconButtonType.Telegram( |  | ||||||
|                             backgroundColor = MaterialTheme.colorScheme.primaryContainer alwaysLight true, |  | ||||||
|                         ) { |  | ||||||
|                             view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) |  | ||||||
|                             view.playSoundEffect(SoundEffectConstants.CLICK) |  | ||||||
|                             context.startActivity( |  | ||||||
|                                 Intent( |  | ||||||
|                                     Intent.ACTION_VIEW, |  | ||||||
|                                     Uri.parse(telegramLink) |  | ||||||
|                                 ) |  | ||||||
|                             ) |  | ||||||
|                         }) |                         }) | ||||||
|                         Spacer(modifier = Modifier.width(16.dp)) |                         Spacer(modifier = Modifier.width(16.dp)) | ||||||
| 
 | 
 | ||||||
| @ -214,19 +214,34 @@ fun TipsAndSupport( | |||||||
|                             context.startActivity( |                             context.startActivity( | ||||||
|                                 Intent( |                                 Intent( | ||||||
|                                     Intent.ACTION_VIEW, |                                     Intent.ACTION_VIEW, | ||||||
|                                     Uri.parse(githubLink) |                                     Uri.parse(context.getString(R.string.github_link)) | ||||||
|                                 ) |                                 ) | ||||||
|                             ) |                             ) | ||||||
|                         }) |                         }) | ||||||
|                         Spacer(modifier = Modifier.width(16.dp)) |                         Spacer(modifier = Modifier.width(16.dp)) | ||||||
| 
 | 
 | ||||||
|                         // License |                         // Telegram | ||||||
|                         RoundIconButton(RoundIconButtonType.License( |                         RoundIconButton(RoundIconButtonType.Telegram( | ||||||
|  |                             backgroundColor = MaterialTheme.colorScheme.primaryContainer alwaysLight true, | ||||||
|  |                         ) { | ||||||
|  |                             view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) | ||||||
|  |                             view.playSoundEffect(SoundEffectConstants.CLICK) | ||||||
|  |                             context.startActivity( | ||||||
|  |                                 Intent( | ||||||
|  |                                     Intent.ACTION_VIEW, | ||||||
|  |                                     Uri.parse(context.getString(R.string.telegram_link)) | ||||||
|  |                                 ) | ||||||
|  |                             ) | ||||||
|  |                         }) | ||||||
|  |                         Spacer(modifier = Modifier.width(16.dp)) | ||||||
|  | 
 | ||||||
|  |                         // Help | ||||||
|  |                         RoundIconButton(RoundIconButtonType.Help( | ||||||
|                             backgroundColor = MaterialTheme.colorScheme.secondaryContainer alwaysLight true, |                             backgroundColor = MaterialTheme.colorScheme.secondaryContainer alwaysLight true, | ||||||
|                         ) { |                         ) { | ||||||
|                             view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) |                             view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) | ||||||
|                             view.playSoundEffect(SoundEffectConstants.CLICK) |                             view.playSoundEffect(SoundEffectConstants.CLICK) | ||||||
|                             context.showToast(comingSoon) |                             context.showToast(context.getString(R.string.coming_soon)) | ||||||
|                         }) |                         }) | ||||||
|                     } |                     } | ||||||
|                     Spacer(modifier = Modifier.height(48.dp)) |                     Spacer(modifier = Modifier.height(48.dp)) | ||||||
| @ -235,10 +250,7 @@ fun TipsAndSupport( | |||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     UpdateDialog( |     UpdateDialog() | ||||||
|         visible = viewState.updateDialogVisible, |  | ||||||
|         onDismissRequest = { updateViewModel.dispatch(UpdateViewAction.Hide) }, |  | ||||||
|     ) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @Immutable | @Immutable | ||||||
| @ -247,6 +259,7 @@ sealed class RoundIconButtonType( | |||||||
|     val iconVector: ImageVector? = null, |     val iconVector: ImageVector? = null, | ||||||
|     val descResource: Int? = null, |     val descResource: Int? = null, | ||||||
|     val descString: String? = null, |     val descString: String? = null, | ||||||
|  |     open val size: Dp = 24.dp, | ||||||
|     open val offset: Modifier = Modifier.offset(), |     open val offset: Modifier = Modifier.offset(), | ||||||
|     open val backgroundColor: Color = Color.Unspecified, |     open val backgroundColor: Color = Color.Unspecified, | ||||||
|     open val onClick: () -> Unit = {}, |     open val onClick: () -> Unit = {}, | ||||||
| @ -263,6 +276,18 @@ sealed class RoundIconButtonType( | |||||||
|         onClick = onClick, |         onClick = onClick, | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  |     @Immutable | ||||||
|  |     data class GitHub( | ||||||
|  |         val desc: String = "GitHub", | ||||||
|  |         override val backgroundColor: Color, | ||||||
|  |         override val onClick: () -> Unit = {}, | ||||||
|  |     ) : RoundIconButtonType( | ||||||
|  |         iconResource = R.drawable.ic_github, | ||||||
|  |         descString = desc, | ||||||
|  |         backgroundColor = backgroundColor, | ||||||
|  |         onClick = onClick, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|     @Immutable |     @Immutable | ||||||
|     data class Telegram( |     data class Telegram( | ||||||
|         val desc: String = "Telegram", |         val desc: String = "Telegram", | ||||||
| @ -277,24 +302,13 @@ sealed class RoundIconButtonType( | |||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     @Immutable |     @Immutable | ||||||
|     data class GitHub( |     data class Help( | ||||||
|         val desc: String = "GitHub", |         val desc: Int = R.string.help, | ||||||
|  |         override val offset: Modifier = Modifier.offset(x = (3).dp), | ||||||
|         override val backgroundColor: Color, |         override val backgroundColor: Color, | ||||||
|         override val onClick: () -> Unit = {}, |         override val onClick: () -> Unit = {}, | ||||||
|     ) : RoundIconButtonType( |     ) : RoundIconButtonType( | ||||||
|         iconResource = R.drawable.ic_github, |         iconVector = Icons.Rounded.TipsAndUpdates, | ||||||
|         descString = desc, |  | ||||||
|         backgroundColor = backgroundColor, |  | ||||||
|         onClick = onClick, |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     @Immutable |  | ||||||
|     data class License( |  | ||||||
|         val desc: Int = R.string.open_source_licenses, |  | ||||||
|         override val backgroundColor: Color, |  | ||||||
|         override val onClick: () -> Unit = {}, |  | ||||||
|     ) : RoundIconButtonType( |  | ||||||
|         iconVector = Icons.Rounded.Balance, |  | ||||||
|         descResource = desc, |         descResource = desc, | ||||||
|         backgroundColor = backgroundColor, |         backgroundColor = backgroundColor, | ||||||
|         onClick = onClick, |         onClick = onClick, | ||||||
| @ -313,9 +327,9 @@ private fun RoundIconButton(type: RoundIconButtonType) { | |||||||
|         onClick = { type.onClick() } |         onClick = { type.onClick() } | ||||||
|     ) { |     ) { | ||||||
|         when (type) { |         when (type) { | ||||||
|             is RoundIconButtonType.Sponsor, is RoundIconButtonType.License -> { |             is RoundIconButtonType.Sponsor, is RoundIconButtonType.Help -> { | ||||||
|                 Icon( |                 Icon( | ||||||
|                     modifier = type.offset, |                     modifier = type.offset.size(type.size), | ||||||
|                     imageVector = type.iconVector!!, |                     imageVector = type.iconVector!!, | ||||||
|                     contentDescription = stringResource(type.descResource!!), |                     contentDescription = stringResource(type.descResource!!), | ||||||
|                     tint = MaterialTheme.colorScheme.onSurface alwaysLight true, |                     tint = MaterialTheme.colorScheme.onSurface alwaysLight true, | ||||||
| @ -323,7 +337,7 @@ private fun RoundIconButton(type: RoundIconButtonType) { | |||||||
|             } |             } | ||||||
|             is RoundIconButtonType.GitHub, is RoundIconButtonType.Telegram -> { |             is RoundIconButtonType.GitHub, is RoundIconButtonType.Telegram -> { | ||||||
|                 Icon( |                 Icon( | ||||||
|                     modifier = type.offset, |                     modifier = type.offset.size(type.size), | ||||||
|                     painter = painterResource(type.iconResource!!), |                     painter = painterResource(type.iconResource!!), | ||||||
|                     contentDescription = type.descString, |                     contentDescription = type.descString, | ||||||
|                     tint = MaterialTheme.colorScheme.onSurface alwaysLight true, |                     tint = MaterialTheme.colorScheme.onSurface alwaysLight true, | ||||||
|  | |||||||
| @ -1,6 +1,13 @@ | |||||||
| package me.ash.reader.ui.page.settings | package me.ash.reader.ui.page.settings | ||||||
| 
 | 
 | ||||||
|  | import android.Manifest | ||||||
| import android.annotation.SuppressLint | import android.annotation.SuppressLint | ||||||
|  | import android.content.Intent | ||||||
|  | import android.net.Uri | ||||||
|  | import android.provider.Settings | ||||||
|  | import android.util.Log | ||||||
|  | import androidx.activity.compose.rememberLauncherForActivityResult | ||||||
|  | import androidx.activity.result.contract.ActivityResultContracts | ||||||
| import androidx.compose.foundation.layout.Column | import androidx.compose.foundation.layout.Column | ||||||
| import androidx.compose.foundation.layout.Spacer | import androidx.compose.foundation.layout.Spacer | ||||||
| import androidx.compose.foundation.layout.height | import androidx.compose.foundation.layout.height | ||||||
| @ -22,26 +29,25 @@ import androidx.compose.ui.Modifier | |||||||
| import androidx.compose.ui.platform.LocalContext | import androidx.compose.ui.platform.LocalContext | ||||||
| import androidx.compose.ui.res.stringResource | import androidx.compose.ui.res.stringResource | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
|  | import androidx.hilt.navigation.compose.hiltViewModel | ||||||
| import com.google.accompanist.pager.ExperimentalPagerApi | import com.google.accompanist.pager.ExperimentalPagerApi | ||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
| import kotlinx.coroutines.flow.map | import kotlinx.coroutines.flow.map | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
| import me.ash.reader.R | import me.ash.reader.R | ||||||
|  | import me.ash.reader.data.source.Download | ||||||
| import me.ash.reader.ui.component.Dialog | import me.ash.reader.ui.component.Dialog | ||||||
| import me.ash.reader.ui.ext.DataStoreKeys | import me.ash.reader.ui.ext.* | ||||||
| import me.ash.reader.ui.ext.dataStore |  | ||||||
| import me.ash.reader.ui.ext.put |  | ||||||
| 
 | 
 | ||||||
| @SuppressLint("FlowOperatorInvokedInComposition") | @SuppressLint("FlowOperatorInvokedInComposition") | ||||||
| @OptIn(ExperimentalPagerApi::class) | @OptIn(ExperimentalPagerApi::class) | ||||||
| @Composable | @Composable | ||||||
| fun UpdateDialog( | fun UpdateDialog( | ||||||
|     modifier: Modifier = Modifier, |     updateViewModel: UpdateViewModel = hiltViewModel(), | ||||||
|     visible: Boolean = false, |  | ||||||
|     onDismissRequest: () -> Unit = {}, |  | ||||||
|     onConfirm: (String) -> Unit = {}, |  | ||||||
| ) { | ) { | ||||||
|     val context = LocalContext.current |     val context = LocalContext.current | ||||||
|  |     val viewState = updateViewModel.viewState.collectAsStateValue() | ||||||
|  |     val downloadState = viewState.downloadFlow.collectAsState(initial = Download.NotYet).value | ||||||
|     val scope = rememberCoroutineScope { Dispatchers.IO } |     val scope = rememberCoroutineScope { Dispatchers.IO } | ||||||
|     val newVersionNumber = context.dataStore.data |     val newVersionNumber = context.dataStore.data | ||||||
|         .map { it[DataStoreKeys.NewVersionNumber.key] ?: "" } |         .map { it[DataStoreKeys.NewVersionNumber.key] ?: "" } | ||||||
| @ -55,17 +61,38 @@ fun UpdateDialog( | |||||||
|         .map { it[DataStoreKeys.NewVersionLog.key] ?: "" } |         .map { it[DataStoreKeys.NewVersionLog.key] ?: "" } | ||||||
|         .collectAsState(initial = "") |         .collectAsState(initial = "") | ||||||
|         .value |         .value | ||||||
|     val newVersionSize = context.dataStore.data |     val newVersionSize = " " + context.dataStore.data | ||||||
|         .map { it[DataStoreKeys.NewVersionSize.key] ?: 0 } |         .map { it[DataStoreKeys.NewVersionSize.key] ?: 0 } | ||||||
|         .map { it / 1024f / 1024f } |         .map { it / 1024f / 1024f } | ||||||
|         .map { if (it > 0f) " ${String.format("%.2f", it)} MB" else "" } |         .map { if (it > 0f) " ${String.format("%.2f", it)} MB" else "" } | ||||||
|         .collectAsState(initial = 0) |         .collectAsState(initial = 0) | ||||||
|         .value |         .value | ||||||
| 
 | 
 | ||||||
|  |     val settings = rememberLauncherForActivityResult( | ||||||
|  |         ActivityResultContracts.StartActivityForResult() | ||||||
|  |     ) { | ||||||
|  |         context.installLatestApk() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     val launcher = rememberLauncherForActivityResult( | ||||||
|  |         ActivityResultContracts.RequestPermission() | ||||||
|  |     ) { result -> | ||||||
|  |         if (result) { | ||||||
|  |             context.installLatestApk() | ||||||
|  |         } else { | ||||||
|  |             settings.launch( | ||||||
|  |                 Intent( | ||||||
|  |                     Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, | ||||||
|  |                     Uri.parse("package:${context.packageName}"), | ||||||
|  |                 ), | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     Dialog( |     Dialog( | ||||||
|         modifier = modifier.heightIn(max = 400.dp), |         modifier = Modifier.heightIn(max = 400.dp), | ||||||
|         visible = visible, |         visible = viewState.updateDialogVisible, | ||||||
|         onDismissRequest = onDismissRequest, |         onDismissRequest = { updateViewModel.dispatch(UpdateViewAction.Hide) }, | ||||||
|         icon = { |         icon = { | ||||||
|             Icon( |             Icon( | ||||||
|                 imageVector = Icons.Rounded.Update, |                 imageVector = Icons.Rounded.Update, | ||||||
| @ -79,7 +106,7 @@ fun UpdateDialog( | |||||||
|                 Text(text = stringResource(R.string.change_log)) |                 Text(text = stringResource(R.string.change_log)) | ||||||
|                 Spacer(modifier = Modifier.height(16.dp)) |                 Spacer(modifier = Modifier.height(16.dp)) | ||||||
|                 Text( |                 Text( | ||||||
|                     text = newVersionPublishDate, |                     text = "$newVersionPublishDate$newVersionSize", | ||||||
|                     color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), |                     color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), | ||||||
|                     style = MaterialTheme.typography.bodyMedium, |                     style = MaterialTheme.typography.bodyMedium, | ||||||
|                 ) |                 ) | ||||||
| @ -97,20 +124,48 @@ fun UpdateDialog( | |||||||
|         confirmButton = { |         confirmButton = { | ||||||
|             TextButton( |             TextButton( | ||||||
|                 onClick = { |                 onClick = { | ||||||
|  |                     if (downloadState !is Download.Progress) { | ||||||
|  |                         updateViewModel.dispatch( | ||||||
|  |                             UpdateViewAction.DownloadUpdate( | ||||||
|  |                                 url = context.newVersionDownloadUrl, | ||||||
|  |                             ) | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             ) { |             ) { | ||||||
|                 Text(text = stringResource(R.string.update) + newVersionSize) |                 Text( | ||||||
|  |                     text = stringResource(R.string.update) + when (downloadState) { | ||||||
|  |                         is Download.NotYet -> "" | ||||||
|  |                         is Download.Progress -> " ${downloadState.percent}%" | ||||||
|  |                         is Download.Finished -> { | ||||||
|  |                             if (context.packageManager.canRequestPackageInstalls()) { | ||||||
|  |                                 Log.i( | ||||||
|  |                                     "RLog", | ||||||
|  |                                     "Download.Finished: ${downloadState.file.absolutePath}" | ||||||
|  |                                 ) | ||||||
|  |                                 context.installLatestApk() | ||||||
|  |                             } else { | ||||||
|  |                                 launcher.launch(Manifest.permission.REQUEST_INSTALL_PACKAGES) | ||||||
|  |                             } | ||||||
|  |                             "" | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         dismissButton = { |         dismissButton = { | ||||||
|             TextButton(onClick = { |             if (downloadState !is Download.Progress) { | ||||||
|  |                 TextButton( | ||||||
|  |                     onClick = { | ||||||
|                         scope.launch { |                         scope.launch { | ||||||
|                             context.dataStore.put(DataStoreKeys.SkipVersionNumber, newVersionNumber) |                             context.dataStore.put(DataStoreKeys.SkipVersionNumber, newVersionNumber) | ||||||
|                     onDismissRequest() |                             updateViewModel.dispatch(UpdateViewAction.Hide) | ||||||
|                         } |                         } | ||||||
|             }) { |                     } | ||||||
|  |                 ) { | ||||||
|                     Text(text = stringResource(R.string.skip_this_version)) |                     Text(text = stringResource(R.string.skip_this_version)) | ||||||
|                 } |                 } | ||||||
|  |             } | ||||||
|         }, |         }, | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
| @ -3,12 +3,10 @@ package me.ash.reader.ui.page.settings | |||||||
| import androidx.lifecycle.ViewModel | import androidx.lifecycle.ViewModel | ||||||
| import androidx.lifecycle.viewModelScope | import androidx.lifecycle.viewModelScope | ||||||
| import dagger.hilt.android.lifecycle.HiltViewModel | import dagger.hilt.android.lifecycle.HiltViewModel | ||||||
| import kotlinx.coroutines.flow.MutableStateFlow | import kotlinx.coroutines.flow.* | ||||||
| import kotlinx.coroutines.flow.StateFlow |  | ||||||
| import kotlinx.coroutines.flow.asStateFlow |  | ||||||
| import kotlinx.coroutines.flow.update |  | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
| import me.ash.reader.data.repository.AppRepository | import me.ash.reader.data.repository.AppRepository | ||||||
|  | import me.ash.reader.data.source.Download | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| @HiltViewModel | @HiltViewModel | ||||||
| @ -26,6 +24,7 @@ class UpdateViewModel @Inject constructor( | |||||||
|                 action.preProcessor, |                 action.preProcessor, | ||||||
|                 action.postProcessor |                 action.postProcessor | ||||||
|             ) |             ) | ||||||
|  |             is UpdateViewAction.DownloadUpdate -> downloadUpdate(action.url) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -36,11 +35,13 @@ class UpdateViewModel @Inject constructor( | |||||||
|         viewModelScope.launch { |         viewModelScope.launch { | ||||||
|             preProcessor() |             preProcessor() | ||||||
|             appRepository.checkUpdate().let { |             appRepository.checkUpdate().let { | ||||||
|  |                 it?.let { | ||||||
|                     changeUpdateDialogVisible(it) |                     changeUpdateDialogVisible(it) | ||||||
|                     postProcessor(it) |                     postProcessor(it) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     private fun changeUpdateDialogVisible(visible: Boolean) { |     private fun changeUpdateDialogVisible(visible: Boolean) { | ||||||
|         _viewState.update { |         _viewState.update { | ||||||
| @ -49,10 +50,26 @@ class UpdateViewModel @Inject constructor( | |||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private fun downloadUpdate(url: String) { | ||||||
|  |         viewModelScope.launch { | ||||||
|  |             _viewState.update { | ||||||
|  |                 it.copy( | ||||||
|  |                     downloadFlow = flow { emit(Download.Progress(0)) } | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |             _viewState.update { | ||||||
|  |                 it.copy( | ||||||
|  |                     downloadFlow = appRepository.downloadFile(url) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| data class UpdateViewState( | data class UpdateViewState( | ||||||
|     val updateDialogVisible: Boolean = false, |     val updateDialogVisible: Boolean = false, | ||||||
|  |     val downloadFlow: Flow<Download> = emptyFlow(), | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| sealed class UpdateViewAction { | sealed class UpdateViewAction { | ||||||
| @ -63,4 +80,8 @@ sealed class UpdateViewAction { | |||||||
|         val preProcessor: suspend () -> Unit = {}, |         val preProcessor: suspend () -> Unit = {}, | ||||||
|         val postProcessor: suspend (Boolean) -> Unit = {} |         val postProcessor: suspend (Boolean) -> Unit = {} | ||||||
|     ) : UpdateViewAction() |     ) : UpdateViewAction() | ||||||
|  | 
 | ||||||
|  |     data class DownloadUpdate( | ||||||
|  |         val url: String, | ||||||
|  |     ) : UpdateViewAction() | ||||||
| } | } | ||||||
| @ -117,4 +117,8 @@ | |||||||
|     <string name="skip_this_version">跳过这个版本</string> |     <string name="skip_this_version">跳过这个版本</string> | ||||||
|     <string name="checking_updates">正在检查更新…</string> |     <string name="checking_updates">正在检查更新…</string> | ||||||
|     <string name="is_latest_version">已是最新版本</string> |     <string name="is_latest_version">已是最新版本</string> | ||||||
|  |     <string name="check_failure">检查失败</string> | ||||||
|  |     <string name="download_failure">下载失败</string> | ||||||
|  |     <string name="rate_limit">请求速率受限</string> | ||||||
|  |     <string name="help">帮助</string> | ||||||
| </resources> | </resources> | ||||||
| @ -117,4 +117,8 @@ | |||||||
|     <string name="skip_this_version">Skip This Version</string> |     <string name="skip_this_version">Skip This Version</string> | ||||||
|     <string name="checking_updates">Checking for updates…</string> |     <string name="checking_updates">Checking for updates…</string> | ||||||
|     <string name="is_latest_version">This is the latest version</string> |     <string name="is_latest_version">This is the latest version</string> | ||||||
|  |     <string name="check_failure">Check failure</string> | ||||||
|  |     <string name="download_failure">Download failure</string> | ||||||
|  |     <string name="rate_limit">The request rate is limited</string> | ||||||
|  |     <string name="help">Help</string> | ||||||
| </resources> | </resources> | ||||||
							
								
								
									
										4
									
								
								app/src/main/res/xml/file_paths.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/src/main/res/xml/file_paths.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <paths> | ||||||
|  |     <cache-path name="cache" path="."/> | ||||||
|  | </paths> | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user