chore: Merge branch dev to main (#3184)

This commit is contained in:
Ushie
2026-03-24 15:05:40 +03:00
committed by GitHub
31 changed files with 834 additions and 115 deletions

View File

@@ -1,3 +1,31 @@
# app [2.5.0-dev.2](https://github.com/ReVanced/revanced-manager/compare/v2.5.0-dev.1...v2.5.0-dev.2) (2026-03-23)
### Features
* Show release dates and patch count ([#3185](https://github.com/ReVanced/revanced-manager/issues/3185)) ([d5a5ec6](https://github.com/ReVanced/revanced-manager/commit/d5a5ec62a4c57a8b6bc0fcc23c8b74c65be2c66e))
# app [2.5.0-dev.1](https://github.com/ReVanced/revanced-manager/compare/v2.4.1-dev.1...v2.5.0-dev.1) (2026-03-23)
### Bug Fixes
* Update screen crashing ([cd9d2eb](https://github.com/ReVanced/revanced-manager/commit/cd9d2ebd06e0abc5b8b10a752d8c92ca65abc1e3))
* Weird padding in Update screen ([2e96c58](https://github.com/ReVanced/revanced-manager/commit/2e96c58f7bc3afc9fd636b76a2204b1b995f8da6))
### Features
* Improve Updates settings screen ([4ce823c](https://github.com/ReVanced/revanced-manager/commit/4ce823c8c0f4de2bea6c07b362741a980b392e79))
## app [2.4.1-dev.1](https://github.com/ReVanced/revanced-manager/compare/v2.4.0...v2.4.1-dev.1) (2026-03-23)
### Bug Fixes
* Use ease out quart for screen transitions ([b69f7c2](https://github.com/ReVanced/revanced-manager/commit/b69f7c2ba8d320e10c3558681294f8bac93618ae))
* Use em-space and bigger bullet symbols in announcement screen ([2538b6a](https://github.com/ReVanced/revanced-manager/commit/2538b6a7553a1d9366d9f2345a6a471381f18d88))
# app [2.4.0](https://github.com/ReVanced/revanced-manager/compare/v2.3.0...v2.4.0) (2026-03-23)

View File

@@ -1 +1 @@
version = 2.4.0
version = 2.5.0-dev.2

View File

@@ -0,0 +1,493 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "7a99bbb0f5aa995683850a59bdf9f235",
"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, 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"
}
],
"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 , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) 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"
]
},
"indices": [
{
"name": "index_applied_patch_bundle",
"unique": false,
"columnNames": [
"bundle"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_applied_patch_bundle` ON `${TABLE_NAME}` (`bundle`)"
}
],
"foreignKeys": [
{
"table": "installed_app",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"package_name"
],
"referencedColumns": [
"current_package_name"
]
},
{
"table": "patch_bundles",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"bundle"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"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, 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"
}
],
"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, '7a99bbb0f5aa995683850a59bdf9f235')"
]
}
}

View File

@@ -6,8 +6,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.EaseInOutQuad
import androidx.compose.animation.core.EaseOut
import androidx.compose.animation.core.EaseOutQuart
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
@@ -76,7 +75,6 @@ import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : AppCompatActivity() {
@ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
@@ -129,24 +127,24 @@ private fun ReVancedManager(vm: MainViewModel) {
startDestination = startDestination,
enterTransition = {
slideInHorizontally(
animationSpec = tween(300, easing = EaseInOutQuad),
animationSpec = tween(300, easing = EaseOutQuart),
initialOffsetX = { it })
},
exitTransition = {
slideOutHorizontally(
animationSpec = tween(300, easing = EaseOut),
animationSpec = tween(300, easing = EaseOutQuart),
targetOffsetX = { -it / 3 })
},
popEnterTransition = {
slideInHorizontally(
animationSpec = tween(
300,
easing = EaseInOutQuad
easing = EaseOutQuart
), initialOffsetX = { -it / 3 })
},
popExitTransition = {
slideOutHorizontally(
animationSpec = tween(300, easing = EaseOut),
animationSpec = tween(300, easing = EaseOutQuart),
targetOffsetX = { it })
}
) {
@@ -229,7 +227,12 @@ private fun ReVancedManager(vm: MainViewModel) {
UpdateScreen(
onBackClick = navController::popBackStackSafe,
vm = koinViewModel { parametersOf(data.downloadOnScreenEntry) }
vm = koinViewModel {
parametersOf(
ChangelogSource.Manager,
data.downloadOnScreenEntry
)
}
)
}

View File

@@ -26,14 +26,16 @@ 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 = 3,
version = 4,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(
from = 2,
to = 3,
spec = AppDatabase.DeleteTrustedDownloaders::class
)
),
AutoMigration(from = 3, to = 4)
]
)
@TypeConverters(Converters::class)

View File

@@ -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 FROM patch_bundles WHERE uid = :uid")
@Query("SELECT name, version, auto_update, source, released_at FROM patch_bundles WHERE uid = :uid")
suspend fun getProps(uid: Int): SourceProperties?
@Upsert

View File

@@ -10,5 +10,6 @@ data class PatchBundleEntity(
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "version") val versionHash: String? = null,
@ColumnInfo(name = "source") val source: Source,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean,
@ColumnInfo(name = "released_at") val releasedAt: Long? = null,
) : SourceManager.DatabaseEntity

View File

@@ -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 FROM downloaders WHERE uid = :uid")
@Query("SELECT name, version, auto_update, source, released_at FROM downloaders WHERE uid = :uid")
suspend fun getProps(uid: Int): SourceProperties?
@Upsert

View File

@@ -10,5 +10,6 @@ data class DownloaderEntity(
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "version") val versionHash: String? = null,
@ColumnInfo(name = "source") val source: Source,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean,
@ColumnInfo(name = "released_at") val releasedAt: Long? = null
) : SourceManager.DatabaseEntity

View File

@@ -34,5 +34,6 @@ data class SourceProperties(
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "version") val versionHash: String? = null,
@ColumnInfo(name = "source") val source: Source,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean,
@ColumnInfo(name = "released_at") val releasedAt: Long? = null,
)

View File

@@ -26,6 +26,9 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
@@ -114,7 +117,12 @@ abstract class SourceManager<DB : SourceManager.DatabaseEntity, LOADED, OUTPUT>(
val newName = src.loaded?.let(::realNameOf).takeIf { it != src.name }
?: return@syncName
updateDb(uid) { it.copy(name = newName) }
updateDb(uid) {
it.copy(
name = newName,
releasedAt = (src as? RemoteSource)?.releasedAt?.toEpochMillis()
)
}
sources[uid] = src.copy(name = newName)
}
@@ -141,7 +149,7 @@ abstract class SourceManager<DB : SourceManager.DatabaseEntity, LOADED, OUTPUT>(
private suspend fun createEntity(
name: String,
source: SourceInfo,
autoUpdate: Boolean = false
autoUpdate: Boolean = false,
) =
entityFromProps(
uid = generateUid(),
@@ -150,6 +158,7 @@ abstract class SourceManager<DB : SourceManager.DatabaseEntity, LOADED, OUTPUT>(
versionHash = null,
source = source,
autoUpdate = autoUpdate,
releasedAt = null,
)
).also {
dbUpsert(it)
@@ -172,6 +181,7 @@ abstract class SourceManager<DB : SourceManager.DatabaseEntity, LOADED, OUTPUT>(
versionHash = new.versionHash,
source = new.source,
autoUpdate = new.autoUpdate,
releasedAt = new.releasedAt,
)
)
)
@@ -238,7 +248,7 @@ abstract class SourceManager<DB : SourceManager.DatabaseEntity, LOADED, OUTPUT>(
this@SourceManager.store.state.value.sources.values.filterIsInstance<APISource<LOADED>>()
.forEach { src ->
with(src) { deleteLocalFile() }
updateDb(src.uid) { it.copy(versionHash = null) }
updateDb(src.uid) { it.copy(versionHash = null, releasedAt = null) }
}
doReload(state)
@@ -308,15 +318,15 @@ abstract class SourceManager<DB : SourceManager.DatabaseEntity, LOADED, OUTPUT>(
async update@{
Log.d(tag, "Updating: ${it.name}")
val newVersion = it.runCatching {
val updateResult = it.runCatching {
when {
redownload -> downloadLatest()
checkOnly -> getUpdateInfo()?.version
checkOnly -> getUpdateInfo()?.let { info -> RemoteSource.UpdateResult(info.version, info.createdAt) }
else -> update()
} ?: return@update null
}
it to newVersion
it to updateResult
}
}
.awaitAll()
@@ -332,7 +342,7 @@ abstract class SourceManager<DB : SourceManager.DatabaseEntity, LOADED, OUTPUT>(
var hasErrors = false
results.forEach { (src, result) ->
result.getOrNull()?.let { newVersionHash ->
result.getOrNull()?.let { updateResult ->
if (checkOnly) {
outdated.add(src.uid)
return@let
@@ -340,7 +350,11 @@ abstract class SourceManager<DB : SourceManager.DatabaseEntity, LOADED, OUTPUT>(
val name = src.loaded?.let(::realNameOf) ?: src.name
updateDb(src.uid) {
it.copy(versionHash = newVersionHash, name = name)
it.copy(
versionHash = updateResult.versionHash,
name = name,
releasedAt = updateResult.releasedAt.toEpochMillis()
)
}
}
result.exceptionOrNull()?.let {
@@ -378,4 +392,6 @@ abstract class SourceManager<DB : SourceManager.DatabaseEntity, LOADED, OUTPUT>(
interface DatabaseEntity {
val uid: Int
}
}
}
private fun LocalDateTime.toEpochMillis() = toInstant(TimeZone.UTC).toEpochMilliseconds()

View File

@@ -30,9 +30,11 @@ import app.revanced.manager.network.downloader.DownloaderPackage
import app.revanced.manager.util.PM
import dalvik.system.PathClassLoader
import kotlinx.coroutines.flow.map
import kotlinx.datetime.toLocalDateTime
import java.io.File
import java.lang.ref.WeakReference
import java.lang.reflect.Modifier
import kotlin.time.Instant
import app.revanced.manager.data.room.sources.Source as SourceInfo
@OptIn(DownloaderHostApi::class)
@@ -61,7 +63,12 @@ class DownloaderRepository(
override fun loadEntity(entity: DownloaderEntity): Source<DownloaderPackage> = with(entity) {
val file = directoryOf(uid).resolve("downloader.jar")
val actualName =
entity.name.ifEmpty { app.getString(if (uid == 0) R.string.auto_updates_dialog_downloaders else R.string.source_name_fallback) }
name.ifEmpty { app.getString(if (uid == 0) R.string.auto_updates_dialog_downloaders else R.string.source_name_fallback) }
val releasedAt = entity.releasedAt?.let {
Instant.fromEpochMilliseconds(it)
.toLocalDateTime(kotlinx.datetime.TimeZone.UTC)
}
return when (source) {
is SourceInfo.Local -> LocalSource(actualName, uid, null, file, loader)
@@ -69,6 +76,7 @@ class DownloaderRepository(
actualName,
uid,
versionHash,
releasedAt,
null,
file,
SourceInfo.API.SENTINEL,
@@ -80,6 +88,7 @@ class DownloaderRepository(
actualName,
uid,
versionHash,
releasedAt,
null,
file,
source.url.toString(),
@@ -97,7 +106,8 @@ class DownloaderRepository(
name = props.name,
versionHash = props.versionHash,
source = props.source,
autoUpdate = props.autoUpdate
autoUpdate = props.autoUpdate,
releasedAt = props.releasedAt
)
override fun realNameOf(loaded: DownloaderPackage) = loaded.name

View File

@@ -1,22 +1,43 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.BuildConfig
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.utils.getOrThrow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.datetime.LocalDateTime
class ManagerUpdateRepository(
private val reVancedAPI: ReVancedAPI
) {
private val _availableVersion = MutableStateFlow<String?>(null)
val availableVersion = _availableVersion.asStateFlow()
private val asset: ReVancedAsset? = null
private val _releasedAt = MutableStateFlow<LocalDateTime?>(null)
private val _version = MutableStateFlow<String?>(null)
private val _hasUpdate = MutableStateFlow(false)
suspend fun refreshAvailableVersion(): String? {
val version = reVancedAPI.getAppUpdate()?.version
_availableVersion.value = version
return version
val releasedAt = _releasedAt.asStateFlow()
val hasUpdate = _hasUpdate.asStateFlow()
val version = _version.asStateFlow()
suspend fun refresh(): ReVancedAsset {
val update = reVancedAPI.getLatestAppInfo().getOrThrow()
_releasedAt.value = update.createdAt
_version.value = update.version
_hasUpdate.value = update.version.removePrefix("v") != BuildConfig.VERSION_NAME
return update
}
fun clearAvailableVersion() {
_availableVersion.value = null
suspend fun getUpdateOrNull(refetch: Boolean = false): ReVancedAsset? {
val asset = if (refetch || asset == null) refresh() else null
return asset.takeIf { _hasUpdate.value }
}
fun clearState() {
_releasedAt.value = null
_version.value = null
_hasUpdate.value = false
}
}

View File

@@ -15,6 +15,7 @@ import app.revanced.manager.domain.sources.LocalPatchBundle
import app.revanced.manager.domain.sources.PatchBundleSource
import app.revanced.manager.domain.manager.SourceManager
import app.revanced.manager.domain.sources.Loader
import app.revanced.manager.domain.sources.RemotePatchBundle
import app.revanced.manager.domain.sources.Source
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.patcher.patch.PatchBundle
@@ -27,9 +28,13 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import java.io.File
import kotlin.collections.map
import kotlin.text.ifEmpty
import kotlin.time.Instant
private typealias Info = PersistentMap<Int, PatchBundleInfo.Global>
@@ -58,12 +63,18 @@ class PatchBundleRepository(
val actualName =
entity.name.ifEmpty { app.getString(if (uid == 0) R.string.patches_name_default else R.string.source_name_fallback) }
val releasedAt = entity.releasedAt?.let {
Instant.fromEpochMilliseconds(it)
.toLocalDateTime(TimeZone.UTC)
}
return when (source) {
is SourceInfo.Local -> LocalPatchBundle(actualName, uid, null, file, PatchBundleLoader)
is SourceInfo.API -> APIPatchBundle(
actualName,
uid,
versionHash,
releasedAt,
null,
file,
SourceInfo.API.SENTINEL,
@@ -75,6 +86,7 @@ class PatchBundleRepository(
actualName,
uid,
versionHash,
releasedAt,
null,
file,
source.url.toString(),
@@ -92,7 +104,8 @@ class PatchBundleRepository(
name = props.name,
versionHash = props.versionHash,
source = props.source,
autoUpdate = props.autoUpdate
autoUpdate = props.autoUpdate,
releasedAt = props.releasedAt
)
override fun realNameOf(loaded: PatchBundle) = loaded.manifestAttributes?.name
@@ -173,6 +186,7 @@ class PatchBundleRepository(
this[src.uid] = PatchBundleInfo.Global(
src.name,
bundle.manifestAttributes?.version,
(src as? RemotePatchBundle)?.releasedAt,
src.uid,
result.getOrThrow().toList()
)

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
import kotlinx.datetime.LocalDateTime
typealias RemotePatchBundle = RemoteSource<PatchBundle>
typealias JsonPatchBundle = JsonSource<PatchBundle>
@@ -22,17 +23,28 @@ sealed class RemoteSource<T>(
name: String,
uid: Int,
protected val versionHash: String?,
val releasedAt: LocalDateTime?,
error: Throwable?,
file: File,
val endpoint: String,
val autoUpdate: Boolean,
loader: Loader<T>
) : Source<T>(name, uid, error, file, loader), KoinComponent {
data class UpdateResult(val versionHash: String, val releasedAt: LocalDateTime)
protected val http: HttpService by inject()
protected abstract suspend fun getLatestInfo(): ReVancedAsset
abstract fun copy(error: Throwable? = this.error, name: String = this.name, autoUpdate: Boolean = this.autoUpdate): RemoteSource<T>
override fun copy(error: Throwable?, name: String): RemoteSource<T> = copy(error, name, this.autoUpdate)
abstract fun copy(
error: Throwable? = this.error,
name: String = this.name,
autoUpdate: Boolean = this.autoUpdate,
versionHash: String? = this.versionHash,
releasedAt: LocalDateTime? = this.releasedAt
): RemoteSource<T>
override fun copy(error: Throwable?, name: String): RemoteSource<T> =
copy(error, name, this.autoUpdate, this.versionHash, this.releasedAt)
private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
outputStream().use {
@@ -41,15 +53,17 @@ sealed class RemoteSource<T>(
}
}
info.version
UpdateResult(info.version, info.createdAt)
}
/**
* Downloads the latest version regardless if there is a new update available.
*/
suspend fun ActionContext.downloadLatest() = download(getLatestInfo())
suspend fun ActionContext.getUpdateInfo() = getLatestInfo().takeUnless { hasInstalled() && it.version == versionHash }
suspend fun ActionContext.update(): String? = withContext(Dispatchers.IO) {
suspend fun ActionContext.getUpdateInfo() =
getLatestInfo().takeUnless { hasInstalled() && it.version == versionHash }
suspend fun ActionContext.update(): UpdateResult? = withContext(Dispatchers.IO) {
getUpdateInfo()?.let { download(it) }
}
@@ -62,22 +76,30 @@ class JsonSource<T>(
name: String,
uid: Int,
versionHash: String?,
releasedAt: LocalDateTime?,
error: Throwable?,
file: File,
endpoint: String,
autoUpdate: Boolean,
loader: Loader<T>
) : RemoteSource<T>(name, uid, versionHash, error, file, endpoint, autoUpdate, loader) {
) : RemoteSource<T>(name, uid, versionHash, releasedAt, error, file, endpoint, autoUpdate, loader) {
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
http.request<ReVancedAsset> {
url(endpoint)
}.getOrThrow()
}
override fun copy(error: Throwable?, name: String, autoUpdate: Boolean) = JsonSource(
override fun copy(
error: Throwable?,
name: String,
autoUpdate: Boolean,
versionHash: String?,
releasedAt: LocalDateTime?
) = JsonSource(
name,
uid,
versionHash,
releasedAt,
error,
file,
endpoint,
@@ -90,20 +112,28 @@ class APISource<T>(
name: String,
uid: Int,
versionHash: String?,
releasedAt: LocalDateTime?,
error: Throwable?,
file: File,
endpoint: String,
autoUpdate: Boolean,
loader: Loader<T>,
private val getUpdate: suspend ReVancedAPI.() -> APIResponse<ReVancedAsset>
) : RemoteSource<T>(name, uid, versionHash, error, file, endpoint, autoUpdate, loader) {
) : RemoteSource<T>(name, uid, versionHash, releasedAt, error, file, endpoint, autoUpdate, loader) {
private val api: ReVancedAPI by inject()
override suspend fun getLatestInfo() = api.getUpdate().getOrThrow()
override fun copy(error: Throwable?, name: String, autoUpdate: Boolean) = APISource(
override fun copy(
error: Throwable?,
name: String,
autoUpdate: Boolean,
versionHash: String?,
releasedAt: LocalDateTime?
) = APISource(
name,
uid,
versionHash,
releasedAt,
error,
file,
endpoint,

View File

@@ -43,9 +43,6 @@ class ReVancedAPI(
suspend fun getAnnouncements() = request<List<ReVancedAnnouncement>>("announcements")
suspend fun getAppUpdate() =
getLatestAppInfo().getOrThrow().takeIf { it.version.removePrefix("v") != BuildConfig.VERSION_NAME }
suspend fun getLatestAppInfo() =
request<ReVancedAsset>("manager${prefs.useManagerPrereleases.prereleaseString()}")

View File

@@ -1,6 +1,7 @@
package app.revanced.manager.patcher.patch
import app.revanced.manager.util.PatchSelection
import kotlinx.datetime.LocalDateTime
/**
* A base class for storing [PatchBundle] metadata.
@@ -16,6 +17,11 @@ sealed class PatchBundleInfo {
*/
abstract val version: String?
/**
* When this bundle was released. Only applicable for remote bundles.
*/
abstract val releasedAt: LocalDateTime?
/**
* The unique ID of the bundle.
*/
@@ -34,6 +40,7 @@ sealed class PatchBundleInfo {
data class Global(
override val name: String,
override val version: String?,
override val releasedAt: LocalDateTime?,
override val uid: Int,
override val patches: List<PatchInfo>
) : PatchBundleInfo() {
@@ -64,6 +71,7 @@ sealed class PatchBundleInfo {
return Scoped(
name,
this.version,
releasedAt,
uid,
relevantPatches,
compatible,
@@ -85,6 +93,7 @@ sealed class PatchBundleInfo {
data class Scoped(
override val name: String,
override val version: String?,
override val releasedAt: LocalDateTime?,
override val uid: Int,
override val patches: List<PatchInfo>,
val compatible: List<PatchInfo>,

View File

@@ -3,14 +3,13 @@ package app.revanced.manager.ui.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Campaign
@@ -20,13 +19,8 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -41,6 +35,7 @@ import app.revanced.manager.util.relativeTime
fun ChangelogList(
changelogs: LazyPagingItems<ReVancedAssetHistory>,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp)
) {
Box(
modifier = modifier.fillMaxSize(),
@@ -67,7 +62,8 @@ fun ChangelogList(
LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize(),
state = listState
state = listState,
contentPadding = contentPadding
) {
items(
count = changelogs.itemCount,

View File

@@ -21,6 +21,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.dp
@@ -28,6 +30,7 @@ import app.revanced.manager.R
import app.revanced.manager.patcher.patch.PatchBundleInfo
import app.revanced.manager.ui.component.TooltipIconButton
import app.revanced.manager.ui.component.haptics.HapticTriStateCheckbox
import app.revanced.manager.util.relativeTime
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -70,13 +73,24 @@ fun SourceSectionHeader(
Text(text = bundle.name)
},
supportingContent = {
val patchCount = bundle.patches.size
val version = bundle.version?.takeIf { it.isNotBlank() }
val releasedAt = bundle.releasedAt?.relativeTime(LocalContext.current)
if (version == null && loadIssue == null) return@ListItem
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
version?.let {
Text(
text = it,
text = listOf(
// Show release date only when view-only
if (readOnly && releasedAt != null) "v$it\u2002($releasedAt)"
else it,
pluralStringResource(
R.plurals.patch_count,
patchCount,
patchCount
)
).joinToString("\u2002\u2022\u2002"),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}

View File

@@ -63,7 +63,7 @@ fun AnnouncementScreen(
},
subtitle = {
val createDate = announcement.createdAt.toLocalDateTime(TimeZone.UTC).relativeTime(LocalContext.current)
Text("$createDate · ${announcement.author}")
Text("$createDate\u2002\u2022\u2002${announcement.author}")
},
navigationIcon = {
TooltipIconButton(

View File

@@ -47,9 +47,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -58,6 +60,7 @@ import app.revanced.manager.R
import app.revanced.manager.domain.repository.ChangelogSource
import app.revanced.manager.domain.sources.Extensions.asRemoteOrNull
import app.revanced.manager.domain.sources.LocalSource
import app.revanced.manager.domain.sources.RemotePatchBundle
import app.revanced.manager.domain.sources.Source
import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.ConfirmDialog
@@ -69,6 +72,8 @@ import app.revanced.manager.ui.component.haptics.HapticSwitch
import app.revanced.manager.ui.component.settings.SafeguardBooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.BundleInformationViewModel
import app.revanced.manager.util.relativeTime
import java.util.Locale.getDefault
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -125,7 +130,17 @@ fun BundleInformationScreen(
val dot = "\u2022" // •
val emSpace = "\u2002" // en space, roughly half character width
val separator = "$emSpace$dot$emSpace"
Text("$subtitleAuthor$separator$subtitleVersion")
Text(text = buildAnnotatedString {
append("$subtitleAuthor$separator$subtitleVersion")
src.asRemoteOrNull?.releasedAt?.let {
val releaseDate = it.relativeTime(
LocalContext.current
).lowercase(getDefault())
append("\u2002($releaseDate)")
}
})
}
} else {
null

View File

@@ -47,7 +47,6 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
@@ -141,7 +140,8 @@ fun DashboardScreen(
!suggestedVersionSafeguard
}
}
val availableUpdate by vm.availableManagerUpdate.collectAsStateWithLifecycle()
val hasUpdate by vm.hasUpdate.collectAsStateWithLifecycle()
val updateVersion by vm.updateVersion.collectAsStateWithLifecycle()
val androidContext = LocalContext.current
val resources = LocalResources.current
val logoPainter = rememberDrawablePainter(drawable = remember(resources) {
@@ -207,12 +207,12 @@ fun DashboardScreen(
}
var showUpdateDialog by rememberSaveable { mutableStateOf(true) }
if (managerAutoUpdates && showUpdateDialog && showManagerUpdateDialogOnLaunch && availableUpdate != null) {
if (managerAutoUpdates && showUpdateDialog && showManagerUpdateDialogOnLaunch && hasUpdate) {
AvailableUpdateDialog(
onDismiss = { showUpdateDialog = false },
setShowManagerUpdateDialogOnLaunch = vm::setShowManagerUpdateDialogOnLaunch,
onConfirm = onUpdateClick,
newVersion = availableUpdate!!
newVersion = updateVersion!!
)
}
@@ -337,7 +337,7 @@ fun DashboardScreen(
}
},
actions = {
if (availableUpdate != null) {
if (updateVersion != null) {
TooltipIconButton(
onClick = onUpdateClick,
tooltip = stringResource(R.string.update),

View File

@@ -110,12 +110,14 @@ fun UpdateScreen(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues ->
Column(
modifier = Modifier.padding(paddingValues).fillMaxSize(),
modifier = Modifier.fillMaxSize(),
) {
if (vm.state == State.DOWNLOADING)
LinearWavyProgressIndicator(
progress = { vm.downloadProgress },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(top = paddingValues.calculateTopPadding())
.fillMaxWidth(),
)
AnimatedVisibility(visible = vm.showInternetCheckDialog) {
@@ -125,7 +127,10 @@ fun UpdateScreen(
)
}
ChangelogList(changelogs = changelogs, modifier = Modifier.padding(paddingValues))
ChangelogList(
changelogs = changelogs,
contentPadding = paddingValues
)
}
}
}

View File

@@ -11,7 +11,6 @@ import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Update
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.SignalWifiOff
@@ -35,7 +34,9 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
@@ -50,8 +51,10 @@ import app.revanced.manager.ui.component.haptics.HapticSwitch
import app.revanced.manager.ui.component.settings.SafeguardBooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
import app.revanced.manager.util.relativeTime
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import java.util.Locale.getDefault
@OptIn(
ExperimentalMaterial3Api::class,
@@ -105,7 +108,20 @@ fun DownloaderInfoScreen(
MediumFlexibleTopAppBar(
title = { Text(appName) },
subtitle = version.takeIf { it.isNotEmpty() }?.let {
{ Text("v$it") }
{
Text(
text = buildAnnotatedString {
append("v$it")
if (remote?.releasedAt != null) {
val releaseDate = remote.releasedAt.relativeTime(
LocalContext.current
).lowercase(getDefault())
append("\u2002($releaseDate)")
}
}
)
}
},
navigationIcon = {
TooltipIconButton(

View File

@@ -43,11 +43,16 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.domain.sources.Extensions.asRemoteOrNull
import app.revanced.manager.domain.sources.Source
import app.revanced.manager.domain.sources.Source.State
import app.revanced.manager.network.downloader.DownloaderPackage
@@ -62,8 +67,10 @@ import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.sources.ImportSourceDialog
import app.revanced.manager.ui.component.sources.ImportSourceDialogStrings
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
import app.revanced.manager.util.relativeTime
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import java.util.Locale.getDefault
private enum class DownloadsTab(
val titleResId: Int,
@@ -317,16 +324,28 @@ private fun DownloaderItem(
Text(source.name, style = MaterialTheme.typography.bodyLarge)
},
supportingContent = {
val stateText =
when (source.state) {
is State.Available<*> -> null
is State.Failed -> R.string.downloader_state_failed
is State.Missing -> R.string.downloader_state_missing
}
val version = source.loaded?.version
val relativeTime =
(source.asRemoteOrNull)?.releasedAt?.relativeTime(LocalContext.current)
Text(
stringResource(
when (source.state) {
is State.Available<*> -> R.string.downloader_state_loaded
is State.Failed -> R.string.downloader_state_failed
is State.Missing -> R.string.downloader_state_missing
text = buildAnnotatedString {
append(if (relativeTime != null) "v$version\u2002($relativeTime)" else "v$version")
// Error colored state text shown when state isn't available
if (stateText != null) withStyle(SpanStyle(color = MaterialTheme.colorScheme.error)) {
append("\u2002\u2022\u2002")
append(stringResource(stateText))
}
)
},
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
trailingContent = source.loaded?.version?.let { @Composable { Text(it) } }
}
)
}
}

View File

@@ -3,6 +3,7 @@ package app.revanced.manager.ui.screen.settings.update
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -18,7 +19,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Update
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.WorkOutline
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -55,7 +55,7 @@ import app.revanced.manager.ui.component.TooltipIconButton
import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SafeguardBooleanItem
import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel
import app.revanced.manager.util.toast
import app.revanced.manager.util.relativeTime
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
@@ -69,10 +69,11 @@ fun UpdatesSettingsScreen(
vm: UpdatesSettingsViewModel = koinViewModel(),
) {
val context = LocalContext.current
val resources = LocalResources.current
val coroutineScope = rememberCoroutineScope()
var checkingForUpdate by remember { mutableStateOf(false) }
val availableUpdate by vm.availableManagerUpdate.collectAsStateWithLifecycle()
val managerVersion by vm.managerVersion.collectAsStateWithLifecycle()
val hasUpdate by vm.hasUpdate.collectAsStateWithLifecycle()
val updateReleasedAt by vm.updateReleasedAt.collectAsStateWithLifecycle()
val scrollState = androidx.compose.foundation.rememberScrollState()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
canScroll = {
@@ -112,15 +113,14 @@ fun UpdatesSettingsScreen(
enabled = !checkingForUpdate,
onClick = {
coroutineScope.launch {
if (availableUpdate != null) {
if (hasUpdate) {
onUpdateClick()
return@launch
}
checkingForUpdate = true
try {
val version = vm.checkForUpdates()
if (!version.isNullOrEmpty()) onUpdateClick()
if (vm.checkUpdates()) onUpdateClick()
} finally {
checkingForUpdate = false
}
@@ -144,7 +144,7 @@ fun UpdatesSettingsScreen(
text = stringResource(
when {
checkingForUpdate -> R.string.update_check
availableUpdate != null -> R.string.view_update
hasUpdate -> R.string.view_update
else -> R.string.manual_update_check
}
)
@@ -164,7 +164,13 @@ fun UpdatesSettingsScreen(
) {
ListSection(
title = stringResource(R.string.manager),
leadingContent = { Icon(Icons.Outlined.WorkOutline, contentDescription = null, modifier = Modifier.size(18.dp)) }
leadingContent = {
Icon(
Icons.Outlined.WorkOutline,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
}
) {
Surface(
shape = RoundedCornerShape(4.dp),
@@ -190,11 +196,27 @@ fun UpdatesSettingsScreen(
.padding(start = 4.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
managerVersion?.let { version ->
updateReleasedAt?.let {
val releasedAt = it.relativeTime(LocalContext.current)
Text(
text = "$version\u2002\u2022\u2002$releasedAt",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
Spacer(modifier = Modifier.height(4.dp))
Button(
@@ -242,6 +264,7 @@ fun UpdatesSettingsScreen(
coroutineScope.launch {
vm.useManagerPrereleases.update(value)
vm.clearAvailableManagerUpdate()
vm.checkUpdates(false)
}
}
)

View File

@@ -40,7 +40,8 @@ class DashboardViewModel(
val bundleDownloadError = patchBundleRepository.apiOutageError
private val contentResolver: ContentResolver = app.contentResolver
val availableManagerUpdate = managerUpdateRepository.availableVersion
val hasUpdate = managerUpdateRepository.hasUpdate
val updateVersion = managerUpdateRepository.version
val sourcesNotDownloaded = patchBundleRepository.bundleInfoFlow.map { it.isEmpty() }
val sourceUpdatesAvailable = combine(
@@ -76,7 +77,7 @@ class DashboardViewModel(
if (!prefs.managerAutoUpdates.get()) return
uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") {
managerUpdateRepository.refreshAvailableVersion()
managerUpdateRepository.refresh()
}
}

View File

@@ -18,6 +18,7 @@ import app.revanced.manager.domain.sources.PatchBundleSource
import app.revanced.manager.domain.sources.Source.State
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.sources.Extensions.asRemoteOrNull
import app.revanced.manager.domain.sources.Extensions.version
import app.revanced.manager.patcher.patch.PatchBundleInfo
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
@@ -375,6 +376,7 @@ private fun PatchSelection.toPersistentPatchSelection(): PersistentPatchSelectio
private fun PatchBundleInfo.Global.asReadonlyScoped() = PatchBundleInfo.Scoped(
name = name,
version = version,
releasedAt = releasedAt,
uid = uid,
patches = patches,
compatible = patches,
@@ -385,6 +387,7 @@ private fun PatchBundleInfo.Global.asReadonlyScoped() = PatchBundleInfo.Scoped(
private fun PatchBundleSource.emptyScopedBundleInfo() = PatchBundleInfo.Scoped(
name = name,
version = version,
releasedAt = (this.asRemoteOrNull)?.releasedAt,
uid = uid,
patches = emptyList(),
compatible = emptyList(),

View File

@@ -19,6 +19,7 @@ import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.repository.ChangelogSource
import app.revanced.manager.domain.repository.ChangelogsRepository
import app.revanced.manager.domain.repository.ManagerUpdateRepository
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.dto.ReVancedAssetHistory
@@ -31,9 +32,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import ru.solrudev.ackpine.installer.InstallFailure
import ru.solrudev.ackpine.installer.PackageInstaller
import ru.solrudev.ackpine.installer.createSession
@@ -44,15 +42,14 @@ import ru.solrudev.ackpine.session.parameters.Confirmation
class UpdateViewModel(
private val api: ReVancedAPI,
private val source: ChangelogSource,
private val downloadOnScreenEntry: Boolean
) : ViewModel(), KoinComponent {
private val app: Application by inject()
private val reVancedAPI: ReVancedAPI by inject()
private val http: HttpService by inject()
private val networkInfo: NetworkInfo by inject()
private val fs: Filesystem by inject()
private val ackpineInstaller: PackageInstaller = get()
private val downloadOnScreenEntry: Boolean,
private val app: Application,
private val http: HttpService,
private val networkInfo: NetworkInfo,
private val fs: Filesystem,
private val ackpineInstaller: PackageInstaller,
private val managerUpdateRepository: ManagerUpdateRepository,
) : ViewModel() {
// TODO: save state to handle process death.
var downloadedSize by mutableLongStateOf(0L)
private set
@@ -85,7 +82,7 @@ class UpdateViewModel(
init {
viewModelScope.launch {
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
releaseInfo = reVancedAPI.getAppUpdate()
releaseInfo = managerUpdateRepository.getUpdateOrNull()
?: throw Exception("No update available")
if (downloadOnScreenEntry) {

View File

@@ -18,22 +18,25 @@ class UpdatesSettingsViewModel(
val managerAutoUpdates = prefs.managerAutoUpdates
val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch
val useManagerPrereleases = prefs.useManagerPrereleases
val availableManagerUpdate = managerUpdateRepository.availableVersion
val managerVersion = managerUpdateRepository.version
val updateReleasedAt = managerUpdateRepository.releasedAt
val hasUpdate = managerUpdateRepository.hasUpdate
suspend fun checkForUpdates(): String? {
var availableVersion: String? = null
suspend fun checkUpdates(showToast: Boolean = true): Boolean {
var hasUpdate = false
uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") {
availableVersion = managerUpdateRepository.refreshAvailableVersion()
if (availableVersion == null)
if (managerUpdateRepository.getUpdateOrNull(true) != null) {
hasUpdate = true
} else if (showToast) {
app.toast(app.getString(R.string.no_update_available))
}
}
return availableVersion
return hasUpdate
}
fun clearAvailableManagerUpdate() {
managerUpdateRepository.clearAvailableVersion()
managerUpdateRepository.clearState()
}
}

View File

@@ -491,6 +491,7 @@ Its only compatible with these versions: %2$s</string>
<string name="prerelease_title">Use pre-releases?</string>
<string name="prereleases_warning">Pre-release versions may be unstable and contain bugs. You may experience crashes, data loss, or other unexpected issues.</string>
<string name="changelog">View changelog</string>
<string name="updated_ago">Updated %s</string>
<string name="changelog_loading">Loading changelog…</string>
<string name="changelog_download_fail">Couldnt download changelog: %s</string>
<string name="battery_optimization_notification">Battery optimizations must be turned off in order for ReVanced Manager to work correctly in the background</string>