feat(about): add "View changes" button with detailed commit history (#5711)
* feat(about): add "View changes" button with detailed commit history Add a new "View changes" button in the About screen that displays a detailed changelog dialog when updates are available. This provides users with better visibility into what changes they will receive when updating. Features added: - New "View changes" button in update section (Available/Downloaded states) - ChangesDialog component showing commit history since current version - Clickable commit cards that open GitHub commit pages in browser - Relative time display (5m, 2h, 3d, etc.) following GitHub's format - Full internationalization support for time strings - Fetches commits using GitHub API and compares with current build - Clean Material 3 UI with proper styling and animations Technical changes: - Extended GitHubService with commits and compare endpoints - Added GitHubCommit and GitHubCompareResponse data models - Enhanced NightlyBuildsRepository with commit fetching capabilities - Added time-relative string resources (time_minutes_ago, time_hours_ago, etc.) - Updated AboutViewModel to handle changes dialog state - Modified UpdateState models to include build numbers - Renamed getCommits to getRepositoryCommits for API consistency - Replaced all hardcoded strings with proper internationalized resources All strings are now properly internationalized to ensure translatability across all supported languages. * style: apply spotless code formatting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor(updater): move changelog logic to ViewModel * feat(updater): improve UI and UX --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: SuperDragonXD <70206496+SuperDragonXD@users.noreply.github.com>
This commit is contained in:
@@ -54,8 +54,25 @@
|
||||
|
||||
<string name="download_update">Download update</string>
|
||||
<string name="install_update">Install update</string>
|
||||
<string name="view_changes">View changes</string>
|
||||
<string name="pro_updated">You\'re up-to-date!</string>
|
||||
|
||||
<!-- Changes dialog strings -->
|
||||
<string name="changes_dialog_title">Updates</string>
|
||||
<string name="changes_dialog_error">Failed to load changes</string>
|
||||
<string name="changes_dialog_no_changes">No changes found</string>
|
||||
<string name="changes_dialog_build_format">Build #%1$d → #%2$d</string>
|
||||
<string name="changes_dialog_commit_info">by %1$s • %2$s • %3$s</string>
|
||||
|
||||
<!-- Time ago strings -->
|
||||
<string name="time_just_now">just now</string>
|
||||
<string name="time_minutes_ago">%dm</string>
|
||||
<string name="time_hours_ago">%dh</string>
|
||||
<string name="time_days_ago">%dd</string>
|
||||
<string name="time_weeks_ago">%dw</string>
|
||||
<string name="time_months_ago">%dmo</string>
|
||||
<string name="time_years_ago">%dy</string>
|
||||
|
||||
<string name="managed_by_lawnchair">Managed by Lawnchair</string>
|
||||
|
||||
<!-- When mentioning settings UI -->
|
||||
|
||||
@@ -28,10 +28,16 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -53,7 +59,9 @@ import app.lawnchair.ui.preferences.components.layout.preferenceGroupItems
|
||||
import app.lawnchair.ui.preferences.navigation.AboutLicenses
|
||||
import com.android.launcher3.BuildConfig
|
||||
import com.android.launcher3.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun About(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -62,6 +70,30 @@ fun About(
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
|
||||
val sheetState = rememberModalBottomSheetState(true)
|
||||
var openBottomSheet by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
if (openBottomSheet) {
|
||||
val updateState = uiState.updateState
|
||||
if (updateState is UpdateState.Available) {
|
||||
ChangesDialog(
|
||||
changelogState = updateState.changelogState,
|
||||
onDismiss = {
|
||||
scope.launch {
|
||||
sheetState.hide()
|
||||
}.invokeOnCompletion {
|
||||
openBottomSheet = false
|
||||
}
|
||||
},
|
||||
onDownload = {
|
||||
viewModel.downloadUpdate()
|
||||
},
|
||||
sheetState = sheetState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PreferenceLayoutLazyColumn(
|
||||
label = stringResource(id = R.string.about_label),
|
||||
modifier = modifier,
|
||||
@@ -125,7 +157,15 @@ fun About(
|
||||
item {
|
||||
UpdateSection(
|
||||
updateState = uiState.updateState,
|
||||
onEvent = viewModel::onEvent,
|
||||
onInstall = {
|
||||
viewModel.installUpdate(it)
|
||||
},
|
||||
onViewChanges = {
|
||||
openBottomSheet = true
|
||||
scope.launch {
|
||||
sheetState.show()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
item {
|
||||
|
||||
@@ -104,7 +104,7 @@ sealed interface UpdateState {
|
||||
* @param name The name of the available update (used in `Available` state).
|
||||
* @param url The URL to download the update from (used in `Available` state).
|
||||
*/
|
||||
data class Available(val name: String, val url: String) : UpdateState
|
||||
data class Available(val name: String, val url: String, val changelogState: ChangelogState?) : UpdateState
|
||||
|
||||
/**
|
||||
* An update is currently being downloaded. Contains the download progress.
|
||||
@@ -122,16 +122,11 @@ sealed interface UpdateState {
|
||||
data object Failed : UpdateState
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed interface representing user actions that can be triggered from the "About" screen.
|
||||
*
|
||||
* This interface defines the different types of events related to user interactions,
|
||||
* such as initiating a download or an installation.
|
||||
*/
|
||||
sealed interface AboutEvent {
|
||||
data object OnDownloadClicked : AboutEvent
|
||||
data class OnInstallClicked(val file: File) : AboutEvent
|
||||
}
|
||||
data class ChangelogState(
|
||||
val commits: List<GitHubCommit> = emptyList(),
|
||||
val currentBuildNumber: Int = 0,
|
||||
val latestBuildNumber: Int = 0,
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents the status of a contributor.
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.android.launcher3.BuildConfig
|
||||
import com.android.launcher3.R
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -16,9 +17,11 @@ class AboutViewModel(
|
||||
application: Application,
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
private val api: GitHubService = gitHubApiRetrofit.create()
|
||||
|
||||
private val nightlyBuildsRepository = NightlyBuildsRepository(
|
||||
applicationContext = application,
|
||||
api = gitHubApiRetrofit.create(),
|
||||
api = api,
|
||||
)
|
||||
|
||||
private val _uiState = MutableStateFlow(AboutUiState())
|
||||
@@ -56,11 +59,12 @@ class AboutViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun onEvent(event: AboutEvent) {
|
||||
when (event) {
|
||||
is AboutEvent.OnDownloadClicked -> nightlyBuildsRepository.downloadUpdate()
|
||||
is AboutEvent.OnInstallClicked -> nightlyBuildsRepository.installUpdate(event.file)
|
||||
}
|
||||
fun downloadUpdate() {
|
||||
nightlyBuildsRepository.downloadUpdate()
|
||||
}
|
||||
|
||||
fun installUpdate(file: File) {
|
||||
nightlyBuildsRepository.installUpdate(file)
|
||||
}
|
||||
|
||||
private suspend fun fetchActiveContributors(): Set<String> {
|
||||
@@ -70,6 +74,7 @@ class AboutViewModel(
|
||||
.toSet()
|
||||
}.getOrDefault(emptySet())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val team = listOf(
|
||||
TeamMember(
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
package app.lawnchair.ui.preferences.about
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import app.lawnchair.ui.preferences.components.layout.PreferenceGroupItem
|
||||
import app.lawnchair.ui.preferences.components.layout.PreferenceTemplate
|
||||
import com.android.launcher3.R
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChangesDialog(
|
||||
changelogState: ChangelogState?,
|
||||
sheetState: SheetState,
|
||||
onDismiss: () -> Unit,
|
||||
onDownload: () -> Unit,
|
||||
) {
|
||||
val commits = changelogState?.commits
|
||||
val currentBuild = changelogState?.currentBuildNumber ?: 0
|
||||
val latestBuild = changelogState?.latestBuildNumber ?: 0
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.changes_dialog_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.changes_dialog_build_format,
|
||||
currentBuild,
|
||||
latestBuild,
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp),
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.heightIn(max = 600.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
if (commits != null) {
|
||||
itemsIndexed(commits) { index, commit ->
|
||||
PreferenceGroupItem(
|
||||
cutTop = index != 0,
|
||||
cutBottom = index != commits.lastIndex,
|
||||
) {
|
||||
CommitItem(commit = commit)
|
||||
}
|
||||
Spacer(Modifier.height(3.dp))
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.changes_dialog_error),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onDismiss,
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
onDownload()
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.download_update))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CommitItem(
|
||||
commit: GitHubCommit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val message = commit.commit.message
|
||||
val title = message.substringBefore("\n").take(100)
|
||||
val description = message.substringAfter("\n", "").take(200)
|
||||
|
||||
PreferenceTemplate(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
description = {
|
||||
if (description.isNotEmpty()) {
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
val timeAgo = getTimeAgo(context, commit.commit.author.date)
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.changes_dialog_commit_info,
|
||||
commit.commit.author.name,
|
||||
commit.sha.take(7),
|
||||
timeAgo,
|
||||
),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
)
|
||||
},
|
||||
modifier = modifier.clickable {
|
||||
openCommitInBrowser(context, commit.sha)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun openCommitInBrowser(context: Context, commitSha: String) {
|
||||
val commitUrl = "https://github.com/LawnchairLauncher/lawnchair/commit/$commitSha"
|
||||
val intent = Intent(Intent.ACTION_VIEW, commitUrl.toUri())
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun getTimeAgo(context: Context, dateString: String): String {
|
||||
return try {
|
||||
val commitDate = Instant.parse(dateString)
|
||||
val currentTime = Instant.now()
|
||||
val diffMillis = currentTime.toEpochMilli() - commitDate.toEpochMilli()
|
||||
|
||||
when {
|
||||
diffMillis < TimeUnit.MINUTES.toMillis(1) -> {
|
||||
context.getString(R.string.time_just_now)
|
||||
}
|
||||
|
||||
diffMillis < TimeUnit.HOURS.toMillis(1) -> {
|
||||
val minutes = TimeUnit.MILLISECONDS.toMinutes(diffMillis)
|
||||
context.getString(R.string.time_minutes_ago, minutes)
|
||||
}
|
||||
|
||||
diffMillis < TimeUnit.DAYS.toMillis(1) -> {
|
||||
val hours = TimeUnit.MILLISECONDS.toHours(diffMillis)
|
||||
context.getString(R.string.time_hours_ago, hours)
|
||||
}
|
||||
|
||||
diffMillis < TimeUnit.DAYS.toMillis(7) -> {
|
||||
val days = TimeUnit.MILLISECONDS.toDays(diffMillis)
|
||||
context.getString(R.string.time_days_ago, days)
|
||||
}
|
||||
|
||||
diffMillis < TimeUnit.DAYS.toMillis(30) -> {
|
||||
val weeks = TimeUnit.MILLISECONDS.toDays(diffMillis) / 7
|
||||
context.getString(R.string.time_weeks_ago, weeks)
|
||||
}
|
||||
|
||||
diffMillis < TimeUnit.DAYS.toMillis(365) -> {
|
||||
val months = TimeUnit.MILLISECONDS.toDays(diffMillis) / 30
|
||||
context.getString(R.string.time_months_ago, months)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val years = TimeUnit.MILLISECONDS.toDays(diffMillis) / 365
|
||||
context.getString(R.string.time_years_ago, years)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// If date parsing fails, fallback to date prefix
|
||||
dateString.substringBefore("T")
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,20 @@ interface GitHubService {
|
||||
@Path("repo") repo: String,
|
||||
): List<GitHubEvent>
|
||||
|
||||
@GET("repos/{owner}/{repo}/commits")
|
||||
suspend fun getRepositoryCommits(
|
||||
@Path("owner") owner: String,
|
||||
@Path("repo") repo: String,
|
||||
): List<GitHubCommit>
|
||||
|
||||
@GET("repos/{owner}/{repo}/compare/{base}...{head}")
|
||||
suspend fun compareCommits(
|
||||
@Path("owner") owner: String,
|
||||
@Path("repo") repo: String,
|
||||
@Path("base") base: String,
|
||||
@Path("head") head: String,
|
||||
): GitHubCompareResponse
|
||||
|
||||
@Streaming
|
||||
@GET
|
||||
suspend fun downloadFile(@Url fileUrl: String): ResponseBody
|
||||
@@ -84,6 +98,53 @@ data class GitHubEvent(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a GitHub commit.
|
||||
*
|
||||
* @property sha The SHA hash of the commit.
|
||||
* @property commit The commit details.
|
||||
* @property author The author of the commit.
|
||||
*/
|
||||
@Serializable
|
||||
data class GitHubCommit(
|
||||
val sha: String,
|
||||
val commit: CommitDetails,
|
||||
val author: Author? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class CommitDetails(
|
||||
val message: String,
|
||||
val author: CommitAuthor,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CommitAuthor(
|
||||
val name: String,
|
||||
val date: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Author(
|
||||
val login: String,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a GitHub comparison response between two commits.
|
||||
*
|
||||
* @property commits List of commits between the base and head.
|
||||
* @property aheadBy Number of commits ahead.
|
||||
* @property behindBy Number of commits behind.
|
||||
*/
|
||||
@Serializable
|
||||
data class GitHubCompareResponse(
|
||||
val commits: List<GitHubCommit>,
|
||||
@SerialName("ahead_by")
|
||||
val aheadBy: Int,
|
||||
@SerialName("behind_by")
|
||||
val behindBy: Int,
|
||||
)
|
||||
|
||||
internal val gitHubApiRetrofit: Retrofit by lazy {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
|
||||
@@ -30,6 +30,10 @@ class NightlyBuildsRepository(
|
||||
private val _updateState = MutableStateFlow<UpdateState>(UpdateState.UpToDate)
|
||||
val updateState = _updateState.asStateFlow()
|
||||
|
||||
private var currentBuildNumber: Int = 0
|
||||
private var latestBuildNumber: Int = 0
|
||||
private var currentCommitHash: String = BuildConfig.COMMIT_HASH
|
||||
|
||||
fun checkForUpdate() {
|
||||
coroutineScope.launch(Dispatchers.Default) {
|
||||
_updateState.update { UpdateState.Checking }
|
||||
@@ -42,18 +46,29 @@ class NightlyBuildsRepository(
|
||||
// <major>.<branch>.(#<CI build number>)
|
||||
// This is done inside build.gradle in the source root. Reflect
|
||||
// changes from there if needed.
|
||||
val currentVersion = BuildConfig.VERSION_DISPLAY_NAME
|
||||
currentBuildNumber = BuildConfig.VERSION_DISPLAY_NAME
|
||||
.substringAfterLast("#")
|
||||
.removeSuffix(")")
|
||||
.toIntOrNull() ?: 0
|
||||
val latestVersion =
|
||||
latestBuildNumber =
|
||||
asset?.name?.substringAfter("_")?.substringBefore("-")?.toIntOrNull() ?: 0
|
||||
|
||||
if (asset != null && latestVersion > currentVersion) {
|
||||
if (asset != null && latestBuildNumber > currentBuildNumber) {
|
||||
val commitList = getCommitsSinceCurrentVersion()
|
||||
|
||||
_updateState.update {
|
||||
UpdateState.Available(
|
||||
asset.name,
|
||||
asset.browserDownloadUrl,
|
||||
changelogState = if (commitList != null) {
|
||||
ChangelogState(
|
||||
commits = commitList,
|
||||
currentBuildNumber = currentBuildNumber,
|
||||
latestBuildNumber = latestBuildNumber,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -114,6 +129,27 @@ class NightlyBuildsRepository(
|
||||
applicationContext.startActivity(intent)
|
||||
}
|
||||
|
||||
private suspend fun getCommitsSinceCurrentVersion(): List<GitHubCommit>? {
|
||||
return try {
|
||||
// Get the latest commits (last 100)
|
||||
val commits = api.getRepositoryCommits("LawnchairLauncher", "lawnchair")
|
||||
|
||||
// Find the index of current commit
|
||||
val currentIndex = commits.indexOfFirst { it.sha.startsWith(currentCommitHash) }
|
||||
|
||||
if (currentIndex > 0) {
|
||||
// Return all commits newer than current version
|
||||
commits.take(currentIndex)
|
||||
} else {
|
||||
// If current commit not found, show last N commits
|
||||
commits.take(MAX_FALLBACK_COMMITS)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get commits", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadApk(url: String, onProgress: (Float) -> Unit): File? {
|
||||
return try {
|
||||
val cacheDir = applicationContext.cacheDir
|
||||
@@ -170,3 +206,5 @@ private fun Context.requestInstallPermission() {
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private const val MAX_FALLBACK_COMMITS = 30
|
||||
|
||||
@@ -14,11 +14,13 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.android.launcher3.R
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun UpdateSection(
|
||||
updateState: UpdateState,
|
||||
onEvent: (AboutEvent) -> Unit,
|
||||
onViewChanges: () -> Unit,
|
||||
onInstall: (File) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
@@ -40,8 +42,7 @@ fun UpdateSection(
|
||||
}
|
||||
is UpdateState.Available -> {
|
||||
Button(
|
||||
onClick = { onEvent(AboutEvent.OnDownloadClicked) },
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
onClick = onViewChanges,
|
||||
) {
|
||||
Text(text = stringResource(R.string.download_update))
|
||||
}
|
||||
@@ -49,7 +50,9 @@ fun UpdateSection(
|
||||
is UpdateState.Downloading -> {
|
||||
LinearProgressIndicator(
|
||||
progress = { updateState.progress },
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = "${(updateState.progress * 100).toInt()}%",
|
||||
@@ -58,8 +61,9 @@ fun UpdateSection(
|
||||
}
|
||||
is UpdateState.Downloaded -> {
|
||||
Button(
|
||||
onClick = { onEvent(AboutEvent.OnInstallClicked(updateState.file)) },
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
onClick = {
|
||||
onInstall(updateState.file)
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.install_update))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user