From 2c3fd25f668182511614defa3df176717ebceb8e Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Mon, 6 Apr 2026 21:15:59 +0100 Subject: [PATCH 01/11] feat(SurfaceChip): Allowed configurable colors --- .../app/revanced/manager/ui/component/SurfaceChip.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/component/SurfaceChip.kt b/app/src/main/java/app/revanced/manager/ui/component/SurfaceChip.kt index f9936d3741..f552c7b376 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/SurfaceChip.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/SurfaceChip.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.revanced.manager.R @@ -16,12 +17,14 @@ import com.eygraber.compose.placeholder.placeholder @Composable fun SurfaceChip( - text: String? = null + text: String? = null, + color: Color? = null, + contentColor: Color? = null ) { Surface( shape = MaterialTheme.shapes.small, - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + color = color ?: MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + contentColor = contentColor ?: MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.height(24.dp) ) { Box( From 108c7be98d79795f34eaf06cffe87b45792d62c3 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Mon, 6 Apr 2026 21:31:16 +0100 Subject: [PATCH 02/11] feat(RemoteSource): Added github api source --- .../manager/domain/sources/RemoteSource.kt | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt index cc3246712e..e1e05da50d 100644 --- a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt @@ -7,6 +7,7 @@ import app.revanced.manager.network.service.HttpService import app.revanced.manager.network.utils.APIResponse import app.revanced.manager.network.utils.getOrThrow import app.revanced.manager.patcher.patch.PatchBundle +import app.revanced.manager.ui.component.sources.GithubRelease import io.ktor.client.request.url import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -14,6 +15,9 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.File import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Instant typealias RemotePatchBundle = RemoteSource typealias JsonPatchBundle = JsonSource @@ -84,9 +88,40 @@ class JsonSource( loader: Loader ) : RemoteSource(name, uid, versionHash, releasedAt, error, file, endpoint, autoUpdate, loader) { override suspend fun getLatestInfo() = withContext(Dispatchers.IO) { - http.request { - url(endpoint) - }.getOrThrow() + if (!endpoint.endsWith(".json")) { + val githubMatch = Regex("^https://github\\.com/([^/]+)/([^/]+)/releases/download/([^/]+)/(.+)$").find(endpoint) + if (githubMatch != null) { + val owner = githubMatch.groupValues[1] + val repo = githubMatch.groupValues[2] + val tag = githubMatch.groupValues[3] + + try { + val release = http.request { + url("https://api.github.com/repos/$owner/$repo/releases/tags/$tag") + }.getOrThrow() + + val dateStr = release.publishedAt ?: release.createdAt + val date = dateStr?.let { Instant.parse(it).toLocalDateTime(TimeZone.UTC) } + + return@withContext ReVancedAsset( + downloadUrl = endpoint, + version = endpoint.substringAfterLast('/'), + description = release.name ?: "External github asset", + createdAt = date ?: releasedAt ?: LocalDateTime(1970, 1, 1, 0, 0, 0) + ) + } catch (_: Exception) { + // Fallback to boilerplate + } + } + + return@withContext ReVancedAsset( + downloadUrl = endpoint, + version = endpoint.substringAfterLast('/'), + description = "External github asset", + createdAt = releasedAt ?: LocalDateTime(1970, 1, 1, 0, 0, 0) + ) + } + http.request { url(endpoint) }.getOrThrow() } override fun copy( From 6237c73c03d6320e1c30c652fcfc7e1ba26d223e Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Mon, 6 Apr 2026 21:32:23 +0100 Subject: [PATCH 03/11] feat(ImportSourceDialog): Added github remote implementation --- .../component/sources/ImportSourceDialog.kt | 254 ++++++++++++++++-- 1 file changed, 229 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt index 9dac923a4c..a247d1370e 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt @@ -7,7 +7,16 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -15,13 +24,18 @@ import androidx.compose.material.icons.filled.Topic import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import app.revanced.manager.R +import app.revanced.manager.network.service.HttpService +import app.revanced.manager.network.utils.APIResponse import app.revanced.manager.ui.component.AlertDialogExtended +import app.revanced.manager.ui.component.SurfaceChip import app.revanced.manager.ui.component.TextHorizontalPadding import app.revanced.manager.ui.component.TooltipIconButton import app.revanced.manager.ui.component.haptics.HapticCheckbox @@ -29,6 +43,10 @@ import app.revanced.manager.ui.component.haptics.HapticRadioButton import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.BIN_MIMETYPE import app.revanced.manager.util.transparentListItemColors +import io.ktor.client.request.url +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.koin.compose.koinInject private enum class SourceType { Local, @@ -58,6 +76,23 @@ enum class ImportSourceDialogStrings( ), } +@Serializable +data class GithubRelease( + val name: String? = null, + @SerialName("tag_name") val tagName: String, + val prerelease: Boolean, + val assets: List = emptyList(), + @SerialName("published_at") val publishedAt: String? = null, + @SerialName("created_at") val createdAt: String? = null, + @SerialName("target_commitish") val targetCommitish: String = "" +) + +@Serializable +data class GithubAsset( + val name: String, + @SerialName("browser_download_url") val browserDownloadUrl: String +) + @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ImportSourceDialog( @@ -72,6 +107,19 @@ fun ImportSourceDialog( var remoteUrl by rememberSaveable { mutableStateOf("") } var autoUpdate by rememberSaveable { mutableStateOf(true) } + val githubMatch by + remember(remoteUrl) { + derivedStateOf { + Regex("^https://github\\.com/([^/]+)/([^/]+)/?$").find(remoteUrl.trim()) + } + } + val isGithubRepoUrl by + remember(sourceType, githubMatch) { + derivedStateOf { sourceType == SourceType.Remote && githubMatch != null } + } + + var selectedGithubAssetUrl by rememberSaveable { mutableStateOf(null) } + val fileActivityLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> uri?.let { local = it } @@ -84,30 +132,49 @@ fun ImportSourceDialog( } } - val steps = listOf<@Composable () -> Unit>( - { - SelectSourceTypeStep(strings, sourceType) { selectedType -> - sourceType = selectedType - } - }, - { - ImportSourceStep( - strings, - sourceType, - local, - remoteUrl, - autoUpdate, - ::launchFileActivity, - { remoteUrl = it }, - { autoUpdate = it } + val steps = mutableListOf<@Composable () -> Unit>() + + steps.add { + SelectSourceTypeStep(strings, sourceType) { selectedType -> sourceType = selectedType } + } + + steps.add { + ImportSourceStep( + strings, + sourceType, + local, + remoteUrl, + autoUpdate, + ::launchFileActivity, + { remoteUrl = it }, + { autoUpdate = it } + ) + } + + if (isGithubRepoUrl) { + steps.add { + GithubReleaseStep( + owner = githubMatch!!.groupValues[1], + repo = githubMatch!!.groupValues[2], + selectedAssetUrl = selectedGithubAssetUrl, + onAssetSelected = { selectedGithubAssetUrl = it } ) } - ) + } - val inputsAreValid by remember { + val inputsAreValid by remember( + currentStep, + sourceType, + local, + remoteUrl, + isGithubRepoUrl, + selectedGithubAssetUrl + ) { derivedStateOf { - (sourceType == SourceType.Local && local != null) || - (sourceType == SourceType.Remote && remoteUrl.isNotEmpty()) + if (currentStep < steps.lastIndex) return@derivedStateOf true + if (sourceType == SourceType.Local) return@derivedStateOf local != null + if (isGithubRepoUrl) return@derivedStateOf selectedGithubAssetUrl != null + remoteUrl.isNotEmpty() } } @@ -117,7 +184,7 @@ fun ImportSourceDialog( Text(stringResource(strings.title)) }, text = { - steps[currentStep]() + if (currentStep in steps.indices) steps[currentStep]() }, confirmButton = { if (currentStep == steps.lastIndex) { @@ -126,7 +193,11 @@ fun ImportSourceDialog( onClick = { when (sourceType) { SourceType.Local -> local?.let(onLocalSubmit) - SourceType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate) + SourceType.Remote -> onRemoteSubmit( + if (isGithubRepoUrl) selectedGithubAssetUrl!! + else remoteUrl, + autoUpdate + ) } }, shapes = ButtonDefaults.shapes() @@ -134,9 +205,11 @@ fun ImportSourceDialog( Text(stringResource(R.string.add)) } } else { - TextButton(onClick = { currentStep++ }, shapes = ButtonDefaults.shapes()) { - Text(stringResource(R.string.next)) - } + TextButton( + enabled = inputsAreValid, + onClick = { currentStep++ }, + shapes = ButtonDefaults.shapes() + ) { Text(stringResource(R.string.next)) } } }, dismissButton = { @@ -274,4 +347,135 @@ private fun ImportSourceStep( } } } +} + +@Composable +private fun GithubReleaseStep( + owner: String, + repo: String, + selectedAssetUrl: String?, + onAssetSelected: (String) -> Unit +) { + val httpService: HttpService = koinInject() + var releases by remember { mutableStateOf?>(null) } + var error by remember { mutableStateOf(null) } + var showOlderReleases by rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(owner, repo) { + val response = + httpService.request> { + url("https://api.github.com/repos/$owner/$repo/releases") + } + if (response is APIResponse.Success) { + releases = response.data + } else { + error = "Failed to fetch releases" + } + } + + Column { + if (error != null) { + Text( + text = error!!, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(24.dp) + ) + } else if (releases == null) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { + val latestRelease = releases!!.firstOrNull { !it.prerelease } + val latestPrerelease = releases!!.firstOrNull { it.prerelease } + val explicitReleases = listOfNotNull(latestRelease, latestPrerelease) + .distinctBy { it.tagName } + .sortedByDescending { it.publishedAt ?: it.createdAt ?: "" } + + val filteredReleases = if (showOlderReleases) releases!! else explicitReleases + + if (filteredReleases.isEmpty()) { + Text("No releases found", modifier = Modifier.padding(24.dp)) + } else { + LazyColumn( + modifier = Modifier.padding(horizontal = 8.dp), + contentPadding = PaddingValues(bottom = 8.dp) + ) { + filteredReleases.forEachIndexed { index, release -> + val title = release.tagName.ifEmpty { release.name ?: "Unknown" } + + item(key = release.tagName) { + Row( + modifier = Modifier.padding( + start = 16.dp, + top = if (index == 0) 0.dp else 16.dp, + bottom = 4.dp + ), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + SurfaceChip( + text = if (release.prerelease) "pre-release" else "latest", + color = + if (release.prerelease) + Color(0xFFF57F17).copy(alpha = 0.2f) + else Color(0xFF2E7D32).copy(alpha = 0.2f), + contentColor = + if (release.prerelease) Color(0xFFF57F17) + else Color(0xFF2E7D32) + ) + } + } + + items(release.assets, key = { it.browserDownloadUrl }) { asset -> + val isSelectable = asset.name.endsWith(".rvp") || asset.name.endsWith(".apk") + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = isSelectable) { onAssetSelected(asset.browserDownloadUrl) } + .padding(horizontal = 16.dp, vertical = 6.dp) + .alpha(if (isSelectable) 1f else 0.4f), + verticalAlignment = Alignment.CenterVertically + ) { + HapticRadioButton( + selected = selectedAssetUrl == asset.browserDownloadUrl, + onClick = { } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = asset.name, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + + if (releases!!.size > explicitReleases.size) { + item { + TextButton( + onClick = { showOlderReleases = !showOlderReleases }, + modifier = Modifier.padding(start = 8.dp, top = 4.dp) + ) { + Text( + if (showOlderReleases) "Hide older releases" + else "Show older releases" + ) + } + } + } + } + } + } + } } \ No newline at end of file From fef3903bf82f090d8dabe0ec1bce39076c47e281 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Mon, 6 Apr 2026 21:49:47 +0100 Subject: [PATCH 04/11] feat: Moved to strings file --- .../manager/domain/sources/RemoteSource.kt | 7 +++++-- .../ui/component/sources/ImportSourceDialog.kt | 16 ++++++++++------ app/src/main/res/values/strings.xml | 9 +++++++++ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt index e1e05da50d..6d413eccbd 100644 --- a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt @@ -1,5 +1,7 @@ package app.revanced.manager.domain.sources +import android.app.Application +import app.revanced.manager.R import app.revanced.manager.data.redux.ActionContext import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.dto.ReVancedAsset @@ -37,6 +39,7 @@ sealed class RemoteSource( data class UpdateResult(val versionHash: String, val releasedAt: LocalDateTime) protected val http: HttpService by inject() + protected val app: Application by inject() protected abstract suspend fun getLatestInfo(): ReVancedAsset abstract fun copy( @@ -106,7 +109,7 @@ class JsonSource( return@withContext ReVancedAsset( downloadUrl = endpoint, version = endpoint.substringAfterLast('/'), - description = release.name ?: "External github asset", + description = release.name ?: app.getString(R.string.github_external_asset), createdAt = date ?: releasedAt ?: LocalDateTime(1970, 1, 1, 0, 0, 0) ) } catch (_: Exception) { @@ -117,7 +120,7 @@ class JsonSource( return@withContext ReVancedAsset( downloadUrl = endpoint, version = endpoint.substringAfterLast('/'), - description = "External github asset", + description = app.getString(R.string.github_external_asset), createdAt = releasedAt ?: LocalDateTime(1970, 1, 1, 0, 0, 0) ) } diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt index a247d1370e..a7888c95bd 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt @@ -360,6 +360,8 @@ private fun GithubReleaseStep( var releases by remember { mutableStateOf?>(null) } var error by remember { mutableStateOf(null) } var showOlderReleases by rememberSaveable { mutableStateOf(false) } + val fetchFailedMessage = stringResource(R.string.github_releases_fetch_failed) + val unknownLabel = stringResource(R.string.github_release_unknown) LaunchedEffect(owner, repo) { val response = @@ -369,7 +371,7 @@ private fun GithubReleaseStep( if (response is APIResponse.Success) { releases = response.data } else { - error = "Failed to fetch releases" + error = fetchFailedMessage } } @@ -399,14 +401,14 @@ private fun GithubReleaseStep( val filteredReleases = if (showOlderReleases) releases!! else explicitReleases if (filteredReleases.isEmpty()) { - Text("No releases found", modifier = Modifier.padding(24.dp)) + Text(stringResource(R.string.github_releases_none_found), modifier = Modifier.padding(24.dp)) } else { LazyColumn( modifier = Modifier.padding(horizontal = 8.dp), contentPadding = PaddingValues(bottom = 8.dp) ) { filteredReleases.forEachIndexed { index, release -> - val title = release.tagName.ifEmpty { release.name ?: "Unknown" } + val title = release.tagName.ifEmpty { release.name ?: unknownLabel } item(key = release.tagName) { Row( @@ -424,7 +426,7 @@ private fun GithubReleaseStep( ) Spacer(modifier = Modifier.width(8.dp)) SurfaceChip( - text = if (release.prerelease) "pre-release" else "latest", + text = stringResource(if (release.prerelease) R.string.github_release_prerelease else R.string.github_release_latest), color = if (release.prerelease) Color(0xFFF57F17).copy(alpha = 0.2f) @@ -468,8 +470,10 @@ private fun GithubReleaseStep( modifier = Modifier.padding(start = 8.dp, top = 4.dp) ) { Text( - if (showOlderReleases) "Hide older releases" - else "Show older releases" + stringResource( + if (showOlderReleases) R.string.github_hide_older_releases + else R.string.github_show_older_releases + ) ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7ae10283e..5111a33c5f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -580,6 +580,15 @@ It’s only compatible with these versions: %2$s Use pre-releases Use pre-release versions of %s Patches URL + Failed to fetch releases + No releases found + Unknown + latest + pre-release + Show older releases + Hide older releases + External GitHub asset + Failed to update These patches aren’t compatible with the selected app version: %1$s Tap them for more details. From cef97f6bdfed53d1e6bcab7816ee0a6e630d5d61 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Mon, 6 Apr 2026 23:04:22 +0100 Subject: [PATCH 05/11] fix: Removed unused keys Removed `published_at` and `target_commitish`; API returns the results already in order --- .../java/app/revanced/manager/domain/sources/RemoteSource.kt | 3 +-- .../manager/ui/component/sources/ImportSourceDialog.kt | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt index 6d413eccbd..521b3b6f51 100644 --- a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt @@ -103,8 +103,7 @@ class JsonSource( url("https://api.github.com/repos/$owner/$repo/releases/tags/$tag") }.getOrThrow() - val dateStr = release.publishedAt ?: release.createdAt - val date = dateStr?.let { Instant.parse(it).toLocalDateTime(TimeZone.UTC) } + val date = release.createdAt?.let { Instant.parse(it).toLocalDateTime(TimeZone.UTC) } return@withContext ReVancedAsset( downloadUrl = endpoint, diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt index a7888c95bd..7504e94d8f 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt @@ -82,9 +82,7 @@ data class GithubRelease( @SerialName("tag_name") val tagName: String, val prerelease: Boolean, val assets: List = emptyList(), - @SerialName("published_at") val publishedAt: String? = null, - @SerialName("created_at") val createdAt: String? = null, - @SerialName("target_commitish") val targetCommitish: String = "" + @SerialName("created_at") val createdAt: String? = null ) @Serializable @@ -396,7 +394,6 @@ private fun GithubReleaseStep( val latestPrerelease = releases!!.firstOrNull { it.prerelease } val explicitReleases = listOfNotNull(latestRelease, latestPrerelease) .distinctBy { it.tagName } - .sortedByDescending { it.publishedAt ?: it.createdAt ?: "" } val filteredReleases = if (showOlderReleases) releases!! else explicitReleases From 3b8e8ced65f24f88cd2277a9bc2f07cabfbcdf85 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Mon, 6 Apr 2026 23:45:21 +0100 Subject: [PATCH 06/11] fix: Restrict downloader asset selection to APK via content_type Patches has no filter and allows all types --- .../ui/component/sources/ImportSourceDialog.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt index 7504e94d8f..a53db403c8 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt @@ -88,6 +88,7 @@ data class GithubRelease( @Serializable data class GithubAsset( val name: String, + @SerialName("content_type") val contentType: String? = null, @SerialName("browser_download_url") val browserDownloadUrl: String ) @@ -154,6 +155,7 @@ fun ImportSourceDialog( GithubReleaseStep( owner = githubMatch!!.groupValues[1], repo = githubMatch!!.groupValues[2], + strings = strings, selectedAssetUrl = selectedGithubAssetUrl, onAssetSelected = { selectedGithubAssetUrl = it } ) @@ -351,6 +353,7 @@ private fun ImportSourceStep( private fun GithubReleaseStep( owner: String, repo: String, + strings: ImportSourceDialogStrings, selectedAssetUrl: String?, onAssetSelected: (String) -> Unit ) { @@ -436,7 +439,10 @@ private fun GithubReleaseStep( } items(release.assets, key = { it.browserDownloadUrl }) { asset -> - val isSelectable = asset.name.endsWith(".rvp") || asset.name.endsWith(".apk") + val isSelectable = when (strings) { + ImportSourceDialogStrings.PATCHES -> true + ImportSourceDialogStrings.DOWNLOADERS -> asset.contentType == APK_MIMETYPE + } Row( modifier = Modifier .fillMaxWidth() @@ -447,7 +453,8 @@ private fun GithubReleaseStep( ) { HapticRadioButton( selected = selectedAssetUrl == asset.browserDownloadUrl, - onClick = { } + onClick = { }, + enabled = isSelectable ) Spacer(modifier = Modifier.width(8.dp)) Text( From a4710d8f915e449d0a39f3262847e321e0b0aeba Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 5 May 2026 20:29:34 +0100 Subject: [PATCH 07/11] enhance: Changed to fetch latest stable release --- .../component/sources/ImportSourceDialog.kt | 140 ++++++------------ .../ui/screen/BundleInformationScreen.kt | 2 +- .../app/revanced/manager/util/Constants.kt | 3 +- 3 files changed, 48 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt index a53db403c8..f9ff57c2b0 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt @@ -7,26 +7,22 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Topic +import androidx.compose.material.icons.outlined.AttachFile +import androidx.compose.material.icons.outlined.Sell +import androidx.compose.material.icons.outlined.Update import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.Dp @@ -35,18 +31,23 @@ import app.revanced.manager.R import app.revanced.manager.network.service.HttpService import app.revanced.manager.network.utils.APIResponse import app.revanced.manager.ui.component.AlertDialogExtended -import app.revanced.manager.ui.component.SurfaceChip import app.revanced.manager.ui.component.TextHorizontalPadding import app.revanced.manager.ui.component.TooltipIconButton import app.revanced.manager.ui.component.haptics.HapticCheckbox import app.revanced.manager.ui.component.haptics.HapticRadioButton +import app.revanced.manager.ui.screen.TagValue import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.BIN_MIMETYPE +import app.revanced.manager.util.PGP_MIMETYPE +import app.revanced.manager.util.relativeTime import app.revanced.manager.util.transparentListItemColors import io.ktor.client.request.url +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import org.koin.compose.koinInject +import kotlin.time.Instant private enum class SourceType { Local, @@ -156,7 +157,6 @@ fun ImportSourceDialog( owner = githubMatch!!.groupValues[1], repo = githubMatch!!.groupValues[2], strings = strings, - selectedAssetUrl = selectedGithubAssetUrl, onAssetSelected = { selectedGithubAssetUrl = it } ) } @@ -354,15 +354,12 @@ private fun GithubReleaseStep( owner: String, repo: String, strings: ImportSourceDialogStrings, - selectedAssetUrl: String?, onAssetSelected: (String) -> Unit ) { val httpService: HttpService = koinInject() var releases by remember { mutableStateOf?>(null) } var error by remember { mutableStateOf(null) } - var showOlderReleases by rememberSaveable { mutableStateOf(false) } val fetchFailedMessage = stringResource(R.string.github_releases_fetch_failed) - val unknownLabel = stringResource(R.string.github_release_unknown) LaunchedEffect(owner, repo) { val response = @@ -393,94 +390,47 @@ private fun GithubReleaseStep( CircularProgressIndicator() } } else { - val latestRelease = releases!!.firstOrNull { !it.prerelease } - val latestPrerelease = releases!!.firstOrNull { it.prerelease } - val explicitReleases = listOfNotNull(latestRelease, latestPrerelease) - .distinctBy { it.tagName } + val latestRelease = releases?.filter { !it.prerelease } ?: emptyList() - val filteredReleases = if (showOlderReleases) releases!! else explicitReleases - - if (filteredReleases.isEmpty()) { + if (latestRelease.isEmpty()) { Text(stringResource(R.string.github_releases_none_found), modifier = Modifier.padding(24.dp)) } else { - LazyColumn( - modifier = Modifier.padding(horizontal = 8.dp), - contentPadding = PaddingValues(bottom = 8.dp) - ) { - filteredReleases.forEachIndexed { index, release -> - val title = release.tagName.ifEmpty { release.name ?: unknownLabel } + val release = latestRelease.first() - item(key = release.tagName) { - Row( - modifier = Modifier.padding( - start = 16.dp, - top = if (index == 0) 0.dp else 16.dp, - bottom = 4.dp - ), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = title, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(8.dp)) - SurfaceChip( - text = stringResource(if (release.prerelease) R.string.github_release_prerelease else R.string.github_release_latest), - color = - if (release.prerelease) - Color(0xFFF57F17).copy(alpha = 0.2f) - else Color(0xFF2E7D32).copy(alpha = 0.2f), - contentColor = - if (release.prerelease) Color(0xFFF57F17) - else Color(0xFF2E7D32) - ) - } - } - - items(release.assets, key = { it.browserDownloadUrl }) { asset -> - val isSelectable = when (strings) { - ImportSourceDialogStrings.PATCHES -> true - ImportSourceDialogStrings.DOWNLOADERS -> asset.contentType == APK_MIMETYPE - } - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(enabled = isSelectable) { onAssetSelected(asset.browserDownloadUrl) } - .padding(horizontal = 16.dp, vertical = 6.dp) - .alpha(if (isSelectable) 1f else 0.4f), - verticalAlignment = Alignment.CenterVertically - ) { - HapticRadioButton( - selected = selectedAssetUrl == asset.browserDownloadUrl, - onClick = { }, - enabled = isSelectable - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = asset.name, - style = MaterialTheme.typography.bodyMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } + val targetAsset = when (strings) { + ImportSourceDialogStrings.PATCHES -> { + release.assets.filter { it.name.endsWith(".rvp") && it.contentType != PGP_MIMETYPE } } + ImportSourceDialogStrings.DOWNLOADERS -> { + release.assets.filter { it.name.endsWith(".apk") && it.contentType == APK_MIMETYPE } + } + }.firstOrNull() - if (releases!!.size > explicitReleases.size) { - item { - TextButton( - onClick = { showOlderReleases = !showOlderReleases }, - modifier = Modifier.padding(start = 8.dp, top = 4.dp) - ) { - Text( - stringResource( - if (showOlderReleases) R.string.github_hide_older_releases - else R.string.github_show_older_releases - ) - ) - } - } + SideEffect { targetAsset?.let { onAssetSelected(it.browserDownloadUrl) } } + + Column( + modifier = Modifier.padding(start = 24.dp, end = 24.dp) + ) { + if (targetAsset == null) { + Text(stringResource(R.string.github_releases_none_found)) + } else { + TagValue( + icon = Icons.Outlined.AttachFile, + title = "File", + value = targetAsset.name + ) + TagValue( + icon = Icons.Outlined.Sell, + title = "Version", + value = release.name ?: release.tagName + ) + TagValue( + icon = Icons.Outlined.Update, + title = "Updated", + value = release.createdAt?.let { + Instant.parse(it).toLocalDateTime(TimeZone.UTC).relativeTime(LocalContext.current) + } ?: "Unknown" + ) } } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt index 26b4c9f7d9..b96ff5505a 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt @@ -383,7 +383,7 @@ fun BundleInformationScreen( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun TagValue( +internal fun TagValue( icon: ImageVector, title: String, value: String, diff --git a/app/src/main/java/app/revanced/manager/util/Constants.kt b/app/src/main/java/app/revanced/manager/util/Constants.kt index 000da463f6..804fbc20da 100644 --- a/app/src/main/java/app/revanced/manager/util/Constants.kt +++ b/app/src/main/java/app/revanced/manager/util/Constants.kt @@ -5,4 +5,5 @@ const val tag = "ReVanced Manager" const val JAR_MIMETYPE = "application/java-archive" const val APK_MIMETYPE = "application/vnd.android.package-archive" const val JSON_MIMETYPE = "application/json" -const val BIN_MIMETYPE = "application/octet-stream" \ No newline at end of file +const val BIN_MIMETYPE = "application/octet-stream" +const val PGP_MIMETYPE = "application/pgp-keys" \ No newline at end of file From 1ac83eaeee2c48f6d1d06bb97f67ae30ce8d6dcb Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 5 May 2026 20:30:45 +0100 Subject: [PATCH 08/11] fix: Removed unused strings --- app/src/main/res/values/strings.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5111a33c5f..01e88becd7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -582,11 +582,6 @@ It’s only compatible with these versions: %2$s Patches URL Failed to fetch releases No releases found - Unknown - latest - pre-release - Show older releases - Hide older releases External GitHub asset Failed to update These patches aren’t compatible with the selected app version: %1$s From 29b62e5af449cbbd442449dd718aba7a32fb124b Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 5 May 2026 21:18:47 +0100 Subject: [PATCH 09/11] refactor: Optimization and formatting --- .../component/sources/ImportSourceDialog.kt | 74 +++++++++---------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt index f9ff57c2b0..6bbd6bcb9b 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt @@ -126,7 +126,7 @@ fun ImportSourceDialog( } fun launchFileActivity() { - when(strings) { + when (strings) { ImportSourceDialogStrings.PATCHES -> fileActivityLauncher.launch(BIN_MIMETYPE) ImportSourceDialogStrings.DOWNLOADERS -> fileActivityLauncher.launch(APK_MIMETYPE) } @@ -354,7 +354,7 @@ private fun GithubReleaseStep( owner: String, repo: String, strings: ImportSourceDialogStrings, - onAssetSelected: (String) -> Unit + onAssetSelected: (String?) -> Unit ) { val httpService: HttpService = koinInject() var releases by remember { mutableStateOf?>(null) } @@ -391,47 +391,45 @@ private fun GithubReleaseStep( } } else { val latestRelease = releases?.filter { !it.prerelease } ?: emptyList() - - if (latestRelease.isEmpty()) { - Text(stringResource(R.string.github_releases_none_found), modifier = Modifier.padding(24.dp)) - } else { - val release = latestRelease.first() - - val targetAsset = when (strings) { - ImportSourceDialogStrings.PATCHES -> { - release.assets.filter { it.name.endsWith(".rvp") && it.contentType != PGP_MIMETYPE } + val release = latestRelease.firstOrNull() + val targetAsset = release?.let { r -> + when (strings) { + ImportSourceDialogStrings.PATCHES -> r.assets.filter { + it.name.endsWith(".rvp") && it.contentType != PGP_MIMETYPE } - ImportSourceDialogStrings.DOWNLOADERS -> { - release.assets.filter { it.name.endsWith(".apk") && it.contentType == APK_MIMETYPE } + + ImportSourceDialogStrings.DOWNLOADERS -> r.assets.filter { + it.name.endsWith(".apk") && it.contentType == APK_MIMETYPE } }.firstOrNull() + } - SideEffect { targetAsset?.let { onAssetSelected(it.browserDownloadUrl) } } + SideEffect { onAssetSelected(targetAsset?.browserDownloadUrl) } - Column( - modifier = Modifier.padding(start = 24.dp, end = 24.dp) - ) { - if (targetAsset == null) { - Text(stringResource(R.string.github_releases_none_found)) - } else { - TagValue( - icon = Icons.Outlined.AttachFile, - title = "File", - value = targetAsset.name - ) - TagValue( - icon = Icons.Outlined.Sell, - title = "Version", - value = release.name ?: release.tagName - ) - TagValue( - icon = Icons.Outlined.Update, - title = "Updated", - value = release.createdAt?.let { - Instant.parse(it).toLocalDateTime(TimeZone.UTC).relativeTime(LocalContext.current) - } ?: "Unknown" - ) - } + Column( + modifier = Modifier.padding(start = 24.dp, end = 24.dp) + ) { + if (latestRelease.isEmpty() || targetAsset == null) { + Text(stringResource(R.string.github_releases_none_found)) + } else { + TagValue( + icon = Icons.Outlined.AttachFile, + title = "File", + value = targetAsset.name + ) + TagValue( + icon = Icons.Outlined.Sell, + title = "Version", + value = release.name ?: release.tagName + ) + TagValue( + icon = Icons.Outlined.Update, + title = "Updated", + value = release.createdAt?.let { + Instant.parse(it).toLocalDateTime(TimeZone.UTC) + .relativeTime(LocalContext.current) + } ?: "Unknown" + ) } } } From 36e9719ce95bb7313bc9772221b9816dd22e80ba Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 5 May 2026 22:22:29 +0100 Subject: [PATCH 10/11] feat: Use pre-releases --- .../6.json | 485 ++++++++++++++++++ .../revanced/manager/data/room/AppDatabase.kt | 5 +- .../data/room/bundles/PatchBundleDao.kt | 2 +- .../data/room/bundles/PatchBundleEntity.kt | 1 + .../data/room/downloader/DownloaderDao.kt | 2 +- .../data/room/downloader/DownloaderEntity.kt | 3 +- .../manager/data/room/sources/Source.kt | 1 + .../manager/domain/manager/SourceManager.kt | 10 + .../domain/repository/DownloaderRepository.kt | 5 +- .../repository/PatchBundleRepository.kt | 5 +- .../manager/domain/sources/RemoteSource.kt | 37 +- .../settings/SafeguardBooleanItem.kt | 34 +- .../component/sources/ImportSourceDialog.kt | 6 +- .../ui/screen/BundleInformationScreen.kt | 12 + .../screen/settings/DownloadersInfoScreen.kt | 12 + .../viewmodel/BundleInformationViewModel.kt | 7 + .../ui/viewmodel/DownloadsViewModel.kt | 7 + app/src/main/res/values/strings.xml | 3 +- 18 files changed, 611 insertions(+), 26 deletions(-) create mode 100644 app/schemas/app.revanced.manager.data.room.AppDatabase/6.json diff --git a/app/schemas/app.revanced.manager.data.room.AppDatabase/6.json b/app/schemas/app.revanced.manager.data.room.AppDatabase/6.json new file mode 100644 index 0000000000..d85ea456d1 --- /dev/null +++ b/app/schemas/app.revanced.manager.data.room.AppDatabase/6.json @@ -0,0 +1,485 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "6dfe67cc094bf524a3127f555a885097", + "entities": [ + { + "tableName": "patch_bundles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` TEXT, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, `released_at` INTEGER, `use_prereleases` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`uid`))", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionHash", + "columnName": "version", + "affinity": "TEXT" + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "auto_update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releasedAt", + "columnName": "released_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "usePrereleases", + "columnName": "use_prereleases", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + } + }, + { + "tableName": "patch_selections", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patchBundle", + "columnName": "patch_bundle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_patch_selections_patch_bundle_package_name", + "unique": true, + "columnNames": [ + "patch_bundle", + "package_name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)" + } + ], + "foreignKeys": [ + { + "table": "patch_bundles", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "patch_bundle" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "selected_patches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`selection` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`selection`, `patch_name`), FOREIGN KEY(`selection`) REFERENCES `patch_selections`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "selection", + "columnName": "selection", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patchName", + "columnName": "patch_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "selection", + "patch_name" + ] + }, + "foreignKeys": [ + { + "table": "patch_selections", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "selection" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "downloaded_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, `last_used` INTEGER NOT NULL, PRIMARY KEY(`package_name`, `version`))", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUsed", + "columnName": "last_used", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "package_name", + "version" + ] + } + }, + { + "tableName": "installed_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`current_package_name` TEXT NOT NULL, `original_package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `install_type` TEXT NOT NULL, PRIMARY KEY(`current_package_name`))", + "fields": [ + { + "fieldPath": "currentPackageName", + "columnName": "current_package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalPackageName", + "columnName": "original_package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installType", + "columnName": "install_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "current_package_name" + ] + } + }, + { + "tableName": "applied_patch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bundle", + "columnName": "bundle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patchName", + "columnName": "patch_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "package_name", + "bundle", + "patch_name" + ] + }, + "foreignKeys": [ + { + "table": "installed_app", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "package_name" + ], + "referencedColumns": [ + "current_package_name" + ] + } + ] + }, + { + "tableName": "installed_patch_bundle", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle_uid` INTEGER NOT NULL, `bundle_name` TEXT NOT NULL, `bundle_version` TEXT, PRIMARY KEY(`package_name`, `bundle_uid`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bundleUid", + "columnName": "bundle_uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bundleName", + "columnName": "bundle_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bundleVersion", + "columnName": "bundle_version", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "package_name", + "bundle_uid" + ] + }, + "foreignKeys": [ + { + "table": "installed_app", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "package_name" + ], + "referencedColumns": [ + "current_package_name" + ] + } + ] + }, + { + "tableName": "option_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patchBundle", + "columnName": "patch_bundle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_option_groups_patch_bundle_package_name", + "unique": true, + "columnNames": [ + "patch_bundle", + "package_name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_option_groups_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)" + } + ], + "foreignKeys": [ + { + "table": "patch_bundles", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "patch_bundle" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "options", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`group`, `patch_name`, `key`), FOREIGN KEY(`group`) REFERENCES `option_groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patchName", + "columnName": "patch_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "group", + "patch_name", + "key" + ] + }, + "foreignKeys": [ + { + "table": "option_groups", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "group" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "downloaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` TEXT, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, `released_at` INTEGER, `use_prereleases` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`uid`))", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionHash", + "columnName": "version", + "affinity": "TEXT" + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "auto_update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releasedAt", + "columnName": "released_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "usePrereleases", + "columnName": "use_prereleases", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6dfe67cc094bf524a3127f555a885097')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt b/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt index 59bc5c2f01..93bb7c466e 100644 --- a/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt +++ b/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt @@ -26,7 +26,7 @@ import kotlin.random.Random @Database( entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, InstalledPatchBundle::class, OptionGroup::class, Option::class, DownloaderEntity::class], - version = 5, + version = 6, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -36,7 +36,8 @@ import kotlin.random.Random spec = AppDatabase.DeleteTrustedDownloaders::class ), AutoMigration(from = 3, to = 4), - AutoMigration(from = 4, to = 5) + AutoMigration(from = 4, to = 5), + AutoMigration(from = 5, to = 6) ] ) @TypeConverters(Converters::class) diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt index 85dd7ffc3f..08d7d75008 100644 --- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt @@ -23,7 +23,7 @@ interface PatchBundleDao { @Query("DELETE FROM patch_bundles WHERE uid = :uid") suspend fun remove(uid: Int) - @Query("SELECT name, version, auto_update, source, released_at FROM patch_bundles WHERE uid = :uid") + @Query("SELECT name, version, auto_update, source, released_at, use_prereleases FROM patch_bundles WHERE uid = :uid") suspend fun getProps(uid: Int): SourceProperties? @Upsert diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt index 7c4b01177e..fea4528413 100644 --- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt @@ -12,4 +12,5 @@ data class PatchBundleEntity( @ColumnInfo(name = "source") val source: Source, @ColumnInfo(name = "auto_update") val autoUpdate: Boolean, @ColumnInfo(name = "released_at") val releasedAt: Long? = null, + @ColumnInfo(name = "use_prereleases", defaultValue = "0") val usePrereleases: Boolean = false, ) : SourceManager.DatabaseEntity \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/downloader/DownloaderDao.kt b/app/src/main/java/app/revanced/manager/data/room/downloader/DownloaderDao.kt index 344273d729..8ee40b5dc3 100644 --- a/app/src/main/java/app/revanced/manager/data/room/downloader/DownloaderDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/downloader/DownloaderDao.kt @@ -26,7 +26,7 @@ interface DownloaderDao { @Query("DELETE FROM downloaders WHERE uid = :uid") suspend fun remove(uid: Int) - @Query("SELECT name, version, auto_update, source, released_at FROM downloaders WHERE uid = :uid") + @Query("SELECT name, version, auto_update, source, released_at, use_prereleases FROM downloaders WHERE uid = :uid") suspend fun getProps(uid: Int): SourceProperties? @Upsert diff --git a/app/src/main/java/app/revanced/manager/data/room/downloader/DownloaderEntity.kt b/app/src/main/java/app/revanced/manager/data/room/downloader/DownloaderEntity.kt index a0d558002d..e90209d46b 100644 --- a/app/src/main/java/app/revanced/manager/data/room/downloader/DownloaderEntity.kt +++ b/app/src/main/java/app/revanced/manager/data/room/downloader/DownloaderEntity.kt @@ -11,5 +11,6 @@ data class DownloaderEntity( @ColumnInfo(name = "version") val versionHash: String? = null, @ColumnInfo(name = "source") val source: Source, @ColumnInfo(name = "auto_update") val autoUpdate: Boolean, - @ColumnInfo(name = "released_at") val releasedAt: Long? = null + @ColumnInfo(name = "released_at") val releasedAt: Long? = null, + @ColumnInfo(name = "use_prereleases", defaultValue = "0") val usePrereleases: Boolean = false ) : SourceManager.DatabaseEntity \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/sources/Source.kt b/app/src/main/java/app/revanced/manager/data/room/sources/Source.kt index 060ed92674..c87d71b6b1 100644 --- a/app/src/main/java/app/revanced/manager/data/room/sources/Source.kt +++ b/app/src/main/java/app/revanced/manager/data/room/sources/Source.kt @@ -36,4 +36,5 @@ data class SourceProperties( @ColumnInfo(name = "source") val source: Source, @ColumnInfo(name = "auto_update") val autoUpdate: Boolean, @ColumnInfo(name = "released_at") val releasedAt: Long? = null, + @ColumnInfo(name = "use_prereleases") val usePrereleases: Boolean = false, ) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/manager/SourceManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/SourceManager.kt index 6ea6dc0c26..035a08906a 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/SourceManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/SourceManager.kt @@ -182,6 +182,7 @@ abstract class SourceManager( source = new.source, autoUpdate = new.autoUpdate, releasedAt = new.releasedAt, + usePrereleases = new.usePrereleases ) ) ) @@ -263,6 +264,15 @@ abstract class SourceManager( state.copy(sources = state.sources.toMutableMap().also { it[uid] = newSrc }) } + suspend fun RemoteSource.setUsePrereleases(value: Boolean) = + dispatchAction("Set use prereleases ($name, $value)") { state -> + updateDb(uid) { it.copy(usePrereleases = value) } + val newSrc = state.sources[uid]?.asRemoteOrNull?.copy(usePrereleases = value) + ?: return@dispatchAction state + + state.copy(sources = state.sources.toMutableMap().also { it[uid] = newSrc }) + } + suspend fun update( vararg sources: RemoteSource, showToast: Boolean = false, diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderRepository.kt index 1c39aee3f2..0960dd51d4 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderRepository.kt @@ -81,6 +81,7 @@ class DownloaderRepository( file, SourceInfo.API.SENTINEL, autoUpdate, + usePrereleases, loader ) { getDownloaderUpdate() } @@ -93,6 +94,7 @@ class DownloaderRepository( file, source.url.toString(), autoUpdate, + usePrereleases, loader ) } @@ -107,7 +109,8 @@ class DownloaderRepository( versionHash = props.versionHash, source = props.source, autoUpdate = props.autoUpdate, - releasedAt = props.releasedAt + releasedAt = props.releasedAt, + usePrereleases = props.usePrereleases ) override fun realNameOf(loaded: DownloaderPackage) = loaded.name diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt index 4517c2725e..c2a032ae2e 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt @@ -79,6 +79,7 @@ class PatchBundleRepository( file, SourceInfo.API.SENTINEL, autoUpdate, + usePrereleases, PatchBundleLoader ) { getPatchesUpdate() } @@ -91,6 +92,7 @@ class PatchBundleRepository( file, source.url.toString(), autoUpdate, + usePrereleases, PatchBundleLoader ) } @@ -105,7 +107,8 @@ class PatchBundleRepository( versionHash = props.versionHash, source = props.source, autoUpdate = props.autoUpdate, - releasedAt = props.releasedAt + releasedAt = props.releasedAt, + usePrereleases = props.usePrereleases ) override fun realNameOf(loaded: PatchBundle) = loaded.manifestAttributes?.name diff --git a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt index 521b3b6f51..2fc2fab6b0 100644 --- a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt @@ -34,6 +34,7 @@ sealed class RemoteSource( file: File, val endpoint: String, val autoUpdate: Boolean, + val usePrereleases: Boolean, loader: Loader ) : Source(name, uid, error, file, loader), KoinComponent { data class UpdateResult(val versionHash: String, val releasedAt: LocalDateTime) @@ -46,12 +47,13 @@ sealed class RemoteSource( error: Throwable? = this.error, name: String = this.name, autoUpdate: Boolean = this.autoUpdate, + usePrereleases: Boolean = this.usePrereleases, versionHash: String? = this.versionHash, releasedAt: LocalDateTime? = this.releasedAt ): RemoteSource override fun copy(error: Throwable?, name: String): RemoteSource = - copy(error, name, this.autoUpdate, this.versionHash, this.releasedAt) + copy(error, name, this.autoUpdate, this.usePrereleases, this.versionHash, this.releasedAt) private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) { outputStream().use { @@ -88,8 +90,9 @@ class JsonSource( file: File, endpoint: String, autoUpdate: Boolean, + usePrereleases: Boolean, loader: Loader -) : RemoteSource(name, uid, versionHash, releasedAt, error, file, endpoint, autoUpdate, loader) { +) : RemoteSource(name, uid, versionHash, releasedAt, error, file, endpoint, autoUpdate, usePrereleases, loader) { override suspend fun getLatestInfo() = withContext(Dispatchers.IO) { if (!endpoint.endsWith(".json")) { val githubMatch = Regex("^https://github\\.com/([^/]+)/([^/]+)/releases/download/([^/]+)/(.+)$").find(endpoint) @@ -97,18 +100,27 @@ class JsonSource( val owner = githubMatch.groupValues[1] val repo = githubMatch.groupValues[2] val tag = githubMatch.groupValues[3] + val currentFilename = githubMatch.groupValues[4] + val extension = currentFilename.substringAfterLast('.', "") try { - val release = http.request { - url("https://api.github.com/repos/$owner/$repo/releases/tags/$tag") + val releases = http.request> { + url("https://api.github.com/repos/$owner/$repo/releases") }.getOrThrow() - - val date = release.createdAt?.let { Instant.parse(it).toLocalDateTime(TimeZone.UTC) } + + val filteredReleases = if (usePrereleases) releases else releases.filter { !it.prerelease } + val latestRelease = filteredReleases.firstOrNull() + ?: error(app.getString(R.string.github_release_none_found)) + + val targetAsset = latestRelease.assets.firstOrNull { it.name.endsWith(".$extension") } + ?: error(app.getString(R.string.github_asset_none_found)) + + val date = latestRelease.createdAt?.let { Instant.parse(it).toLocalDateTime(TimeZone.UTC) } return@withContext ReVancedAsset( - downloadUrl = endpoint, - version = endpoint.substringAfterLast('/'), - description = release.name ?: app.getString(R.string.github_external_asset), + downloadUrl = targetAsset.browserDownloadUrl, + version = targetAsset.name, + description = latestRelease.name ?: app.getString(R.string.github_external_asset), createdAt = date ?: releasedAt ?: LocalDateTime(1970, 1, 1, 0, 0, 0) ) } catch (_: Exception) { @@ -130,6 +142,7 @@ class JsonSource( error: Throwable?, name: String, autoUpdate: Boolean, + usePrereleases: Boolean, versionHash: String?, releasedAt: LocalDateTime? ) = JsonSource( @@ -141,6 +154,7 @@ class JsonSource( file, endpoint, autoUpdate, + usePrereleases, loader ) } @@ -154,9 +168,10 @@ class APISource( file: File, endpoint: String, autoUpdate: Boolean, + usePrereleases: Boolean = false, loader: Loader, private val getUpdate: suspend ReVancedAPI.() -> APIResponse -) : RemoteSource(name, uid, versionHash, releasedAt, error, file, endpoint, autoUpdate, loader) { +) : RemoteSource(name, uid, versionHash, releasedAt, error, file, endpoint, autoUpdate, usePrereleases, loader) { private val api: ReVancedAPI by inject() override suspend fun getLatestInfo() = api.getUpdate().getOrThrow() @@ -164,6 +179,7 @@ class APISource( error: Throwable?, name: String, autoUpdate: Boolean, + usePrereleases: Boolean, versionHash: String?, releasedAt: LocalDateTime? ) = APISource( @@ -175,6 +191,7 @@ class APISource( file, endpoint, autoUpdate, + usePrereleases, loader, getUpdate ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/SafeguardBooleanItem.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/SafeguardBooleanItem.kt index a9631cce56..a48cfdfe87 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/settings/SafeguardBooleanItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/SafeguardBooleanItem.kt @@ -49,17 +49,39 @@ fun SafeguardBooleanItem( onValueChange: ((Boolean) -> Unit)? = null ) { val value by preference.getAsState() - var showSafeguardWarning by rememberSaveable { - mutableStateOf(false) - } val update = onValueChange ?: { coroutineScope.launch { preference.update(it) } } + SafeguardBooleanItem( + modifier = modifier, + value = value, + default = preference.default, + headline = headline, + description = description, + dialogTitle = dialogTitle, + confirmationText = confirmationText, + onValueChange = { update(it) } + ) +} + +@Composable +fun SafeguardBooleanItem( + modifier: Modifier = Modifier, + value: Boolean, + default: Boolean = false, + @StringRes headline: Int, + description: String, + @StringRes dialogTitle: Int, + @StringRes confirmationText: Int, + onValueChange: (Boolean) -> Unit +) { + var showSafeguardWarning by rememberSaveable { mutableStateOf(false) } + if (showSafeguardWarning) { ConfirmDialog( onDismiss = { showSafeguardWarning = false }, onConfirm = { - update(!value) + onValueChange(!value) showSafeguardWarning = false }, title = stringResource(id = dialogTitle), @@ -72,10 +94,10 @@ fun SafeguardBooleanItem( modifier = modifier, value = value, onValueChange = { - if (it != preference.default) { + if (it != default) { showSafeguardWarning = true } else { - update(it) + onValueChange(it) } }, headline = headline, diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt index 6bbd6bcb9b..46bdda8646 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt @@ -409,8 +409,10 @@ private fun GithubReleaseStep( Column( modifier = Modifier.padding(start = 24.dp, end = 24.dp) ) { - if (latestRelease.isEmpty() || targetAsset == null) { - Text(stringResource(R.string.github_releases_none_found)) + if (latestRelease.isEmpty()) { + Text(stringResource(R.string.github_release_none_found)) + } else if (targetAsset == null) { + Text(stringResource(R.string.github_asset_none_found)) } else { TagValue( icon = Icons.Outlined.AttachFile, diff --git a/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt index b96ff5505a..2a8078adf2 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt @@ -285,6 +285,18 @@ fun BundleInformationScreen( confirmationText = R.string.prereleases_warning, onValueChange = viewModel::updateUsePrereleases ) + } else if (autoUpdate != null) { + SafeguardBooleanItem( + value = src.asRemoteOrNull?.usePrereleases ?: false, + headline = R.string.patches_prereleases, + description = stringResource( + R.string.patches_prereleases_description, + src.name + ), + dialogTitle = R.string.prerelease_title, + confirmationText = R.string.prereleases_warning, + onValueChange = viewModel::setUsePrereleases + ) } endpoint?.takeUnless { src.isDefault }?.let { url -> diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadersInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadersInfoScreen.kt index 1c9efbe4d9..714cc2d446 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadersInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadersInfoScreen.kt @@ -213,6 +213,18 @@ fun DownloaderInfoScreen( confirmationText = R.string.prereleases_warning, onValueChange = viewModel::updateUsePrereleases ) + } else if (remote != null) { + SafeguardBooleanItem( + value = remote.usePrereleases, + headline = R.string.downloader_prereleases, + description = stringResource( + R.string.downloader_prereleases_description, + source.name + ), + dialogTitle = R.string.prerelease_title, + confirmationText = R.string.prereleases_warning, + onValueChange = { viewModel.setExternalSourceUsePrereleases(remote, it) } + ) } remote?.endpoint?.takeUnless { source.isDefault }?.let { url -> diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/BundleInformationViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/BundleInformationViewModel.kt index 3b7ca374cc..30c25b1c09 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/BundleInformationViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/BundleInformationViewModel.kt @@ -38,4 +38,11 @@ class BundleInformationViewModel(uid: Int) : ViewModel(), KoinComponent { prefs.usePatchesPrereleases.update(value) refresh() } + + fun setUsePrereleases(value: Boolean) = viewModelScope.launch { + bundle.first()?.asRemoteOrNull?.let { + patchBundleRepository.run { it.setUsePrereleases(value) } + refresh() + } + } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt index 63d20cd7b0..a7f8e4d20f 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt @@ -116,4 +116,11 @@ class DownloadsViewModel( src.setAutoUpdate(value) } } + + fun setExternalSourceUsePrereleases(src: RemoteSource, value: Boolean) = viewModelScope.launch { + with(downloaderRepository) { + src.setUsePrereleases(value) + } + updateDownloader(src) + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 01e88becd7..f23f580e0f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -581,7 +581,8 @@ It’s only compatible with these versions: %2$s Use pre-release versions of %s Patches URL Failed to fetch releases - No releases found + No suitable release found + No suitable asset found in latest release External GitHub asset Failed to update These patches aren’t compatible with the selected app version: %1$s From abfde0b5e24ff2df7dbe4b6b9f809c0f91c9ee99 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 5 May 2026 22:33:20 +0100 Subject: [PATCH 11/11] refactor: Optimized import code logic --- .../component/sources/ImportSourceDialog.kt | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt index 46bdda8646..b5753da1c3 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt @@ -390,34 +390,29 @@ private fun GithubReleaseStep( CircularProgressIndicator() } } else { - val latestRelease = releases?.filter { !it.prerelease } ?: emptyList() - val release = latestRelease.firstOrNull() - val targetAsset = release?.let { r -> + val latestRelease = releases!!.filter { !it.prerelease } + val bundle = latestRelease.firstOrNull()?.let { release -> when (strings) { - ImportSourceDialogStrings.PATCHES -> r.assets.filter { + ImportSourceDialogStrings.PATCHES -> release.assets.firstOrNull { it.name.endsWith(".rvp") && it.contentType != PGP_MIMETYPE } - - ImportSourceDialogStrings.DOWNLOADERS -> r.assets.filter { + ImportSourceDialogStrings.DOWNLOADERS -> release.assets.firstOrNull { it.name.endsWith(".apk") && it.contentType == APK_MIMETYPE } - }.firstOrNull() + }?.let { asset -> release to asset } } - SideEffect { onAssetSelected(targetAsset?.browserDownloadUrl) } + SideEffect { bundle?.let { (_, asset) -> onAssetSelected(asset.browserDownloadUrl) } } - Column( - modifier = Modifier.padding(start = 24.dp, end = 24.dp) - ) { - if (latestRelease.isEmpty()) { - Text(stringResource(R.string.github_release_none_found)) - } else if (targetAsset == null) { - Text(stringResource(R.string.github_asset_none_found)) - } else { + Column(Modifier.padding(horizontal = 24.dp)) { when { + latestRelease.isEmpty() -> Text(stringResource(R.string.github_release_none_found)) + bundle == null -> Text(stringResource(R.string.github_asset_none_found)) + else -> { + val (release, asset) = bundle TagValue( icon = Icons.Outlined.AttachFile, title = "File", - value = targetAsset.name + value = asset.name ) TagValue( icon = Icons.Outlined.Sell, @@ -433,7 +428,7 @@ private fun GithubReleaseStep( } ?: "Unknown" ) } - } + } } } } } \ No newline at end of file