Merge branch 'compose-dev' into compose/downloader-system

This commit is contained in:
Ax333l 2024-12-18 17:00:51 +01:00
commit d0cd29d625
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
21 changed files with 182 additions and 351 deletions

View File

@ -144,6 +144,7 @@ dependencies {
// KotlinX // KotlinX
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.collection.immutable) implementation(libs.kotlinx.collection.immutable)
implementation(libs.kotlinx.datetime)
// Room // Room
implementation(libs.room.runtime) implementation(libs.room.runtime)

View File

@ -1,11 +1,9 @@
package app.revanced.manager.di package app.revanced.manager.di
import app.revanced.manager.network.service.HttpService import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.service.ReVancedService
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
val serviceModule = module { val serviceModule = module {
singleOf(::ReVancedService)
singleOf(::HttpService) singleOf(::HttpService)
} }

View File

@ -2,7 +2,7 @@ package app.revanced.manager.domain.bundles
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.PatchBundleInfo import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.service.HttpService import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.utils.getOrThrow import app.revanced.manager.network.utils.getOrThrow
import io.ktor.client.request.url import io.ktor.client.request.url
@ -16,17 +16,16 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
PatchBundleSource(name, id, directory) { PatchBundleSource(name, id, directory) {
protected val http: HttpService by inject() protected val http: HttpService by inject()
protected abstract suspend fun getLatestInfo(): PatchBundleInfo protected abstract suspend fun getLatestInfo(): ReVancedAsset
private suspend fun download(info: PatchBundleInfo) = withContext(Dispatchers.IO) { private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
val (version, url) = info
patchBundleOutputStream().use { patchBundleOutputStream().use {
http.streamTo(it) { http.streamTo(it) {
url(url) url(info.downloadUrl)
} }
} }
saveVersion(version) saveVersion(info.version)
reload() reload()
} }
@ -58,7 +57,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) : class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle(name, id, directory, endpoint) { RemotePatchBundle(name, id, directory, endpoint) {
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) { override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
http.request<PatchBundleInfo> { http.request<ReVancedAsset> {
url(endpoint) url(endpoint)
}.getOrThrow() }.getOrThrow()
} }
@ -68,10 +67,5 @@ class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle(name, id, directory, endpoint) { RemotePatchBundle(name, id, directory, endpoint) {
private val api: ReVancedAPI by inject() private val api: ReVancedAPI by inject()
override suspend fun getLatestInfo() = api override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow()
.getLatestRelease("revanced-patches")
.getOrThrow()
.let {
PatchBundleInfo(it.version, it.assets.first { it.name.endsWith(".rvp") }.downloadUrl)
}
} }

View File

@ -2,37 +2,41 @@ package app.revanced.manager.network.api
import android.os.Build import android.os.Build
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.network.dto.ReVancedRelease import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.service.ReVancedService import app.revanced.manager.network.dto.ReVancedGitRepository
import app.revanced.manager.network.dto.ReVancedInfo
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.network.utils.getOrThrow
import app.revanced.manager.network.utils.transform import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import io.ktor.client.request.url
class ReVancedAPI( class ReVancedAPI(
private val service: ReVancedService, private val client: HttpService,
private val prefs: PreferencesManager private val prefs: PreferencesManager
) { ) {
private suspend fun apiUrl() = prefs.api.get() private suspend fun apiUrl() = prefs.api.get()
suspend fun getContributors() = service.getContributors(apiUrl()).transform { it.repositories } private suspend inline fun <reified T> request(api: String, route: String): APIResponse<T> =
withContext(
Dispatchers.IO
) {
client.request {
url("$api/v4/$route")
}
}
suspend fun getLatestRelease(name: String) = private suspend inline fun <reified T> request(route: String) = request<T>(apiUrl(), route)
service.getLatestRelease(apiUrl(), name).transform { it.release }
suspend fun getReleases(name: String) =
service.getReleases(apiUrl(), name).transform { it.releases }
suspend fun getAppUpdate() = suspend fun getAppUpdate() =
getLatestRelease("revanced-manager") getLatestAppInfo().getOrThrow().takeIf { it.version != Build.VERSION.RELEASE }
.getOrThrow()
.takeIf { it.version != Build.VERSION.RELEASE }
suspend fun getInfo(api: String? = null) = service.getInfo(api ?: apiUrl()).transform { it.info } suspend fun getLatestAppInfo() = request<ReVancedAsset>("manager")
suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches")
companion object Extensions { suspend fun getContributors() = request<List<ReVancedGitRepository>>("contributors")
fun ReVancedRelease.findAssetByType(mime: String) =
assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime) suspend fun getInfo(api: String? = null) = request<ReVancedInfo>(api ?: apiUrl(), "about")
}
} }
class MissingAssetException(type: String) : Exception("No asset with type $type")

View File

@ -1,16 +0,0 @@
package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GithubChangelog(
@SerialName("tag_name") val version: String,
@SerialName("body") val body: String,
@SerialName("assets") val assets: List<GithubAsset>
)
@Serializable
data class GithubAsset(
@SerialName("download_count") val downloadCount: Int,
)

View File

@ -1,7 +0,0 @@
package app.revanced.manager.network.dto
import kotlinx.serialization.Serializable
@Serializable
// TODO: replace this
data class PatchBundleInfo(val version: String, val url: String)

View File

@ -0,0 +1,18 @@
package app.revanced.manager.network.dto
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ReVancedAsset (
@SerialName("download_url")
val downloadUrl: String,
@SerialName("created_at")
val createdAt: LocalDateTime,
@SerialName("signature_download_url")
val signatureDownloadUrl: String? = null,
val description: String,
val version: String,
)

View File

@ -3,19 +3,15 @@ package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable
data class ReVancedGitRepositories(
val repositories: List<ReVancedGitRepository>,
)
@Serializable @Serializable
data class ReVancedGitRepository( data class ReVancedGitRepository(
val name: String, val name: String,
val url: String,
val contributors: List<ReVancedContributor>, val contributors: List<ReVancedContributor>,
) )
@Serializable @Serializable
data class ReVancedContributor( data class ReVancedContributor(
@SerialName("login") val username: String, @SerialName("name") val username: String,
@SerialName("avatar_url") val avatarUrl: String, @SerialName("avatar_url") val avatarUrl: String,
) )

View File

@ -1,12 +1,8 @@
package app.revanced.manager.network.dto package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable
data class ReVancedInfoParent(
val info: ReVancedInfo,
)
@Serializable @Serializable
data class ReVancedInfo( data class ReVancedInfo(
val name: String, val name: String,
@ -43,7 +39,8 @@ data class ReVancedDonation(
@Serializable @Serializable
data class ReVancedWallet( data class ReVancedWallet(
val network: String, val network: String,
val currency_code: String, @SerialName("currency_code")
val currencyCode: String,
val address: String, val address: String,
val preferred: Boolean val preferred: Boolean
) )

View File

@ -1,41 +0,0 @@
package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ReVancedLatestRelease(
val release: ReVancedRelease,
)
@Serializable
data class ReVancedReleases(
val releases: List<ReVancedRelease>
)
@Serializable
data class ReVancedRelease(
val metadata: ReVancedReleaseMeta,
val assets: List<Asset>
) {
val version get() = metadata.tag
}
@Serializable
data class ReVancedReleaseMeta(
@SerialName("tag_name") val tag: String,
val name: String,
val draft: Boolean,
val prerelease: Boolean,
@SerialName("created_at") val createdAt: String,
@SerialName("published_at") val publishedAt: String,
val body: String,
)
@Serializable
data class Asset(
val name: String,
@SerialName("download_count") val downloadCount: Int,
@SerialName("browser_download_url") val downloadUrl: String,
@SerialName("content_type") val contentType: String
)

View File

@ -1,43 +0,0 @@
package app.revanced.manager.network.service
import app.revanced.manager.network.dto.ReVancedGitRepositories
import app.revanced.manager.network.dto.ReVancedInfo
import app.revanced.manager.network.dto.ReVancedInfoParent
import app.revanced.manager.network.dto.ReVancedLatestRelease
import app.revanced.manager.network.dto.ReVancedReleases
import app.revanced.manager.network.utils.APIResponse
import io.ktor.client.request.url
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ReVancedService(
private val client: HttpService,
) {
suspend fun getLatestRelease(api: String, repo: String): APIResponse<ReVancedLatestRelease> =
withContext(Dispatchers.IO) {
client.request {
url("$api/v2/$repo/releases/latest")
}
}
suspend fun getReleases(api: String, repo: String): APIResponse<ReVancedReleases> =
withContext(Dispatchers.IO) {
client.request {
url("$api/v2/$repo/releases")
}
}
suspend fun getContributors(api: String): APIResponse<ReVancedGitRepositories> =
withContext(Dispatchers.IO) {
client.request {
url("$api/contributors")
}
}
suspend fun getInfo(api: String): APIResponse<ReVancedInfoParent> =
withContext(Dispatchers.IO) {
client.request {
url("$api/v2/info")
}
}
}

View File

@ -26,7 +26,6 @@ import app.revanced.manager.ui.component.Markdown
fun Changelog( fun Changelog(
markdown: String, markdown: String,
version: String, version: String,
downloadCount: String,
publishDate: String publishDate: String
) { ) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
@ -55,10 +54,6 @@ fun Changelog(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
) { ) {
Tag(
Icons.Outlined.FileDownload,
downloadCount
)
Tag( Tag(
Icons.Outlined.CalendarToday, Icons.Outlined.CalendarToday,
publishDate publishDate

View File

@ -0,0 +1,54 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.bundle.BundleItem
@Composable
fun BundleListScreen(
onDelete: (PatchBundleSource) -> Unit,
onUpdate: (PatchBundleSource) -> Unit,
sources: List<PatchBundleSource>,
selectedSources: SnapshotStateList<PatchBundleSource>,
bundlesSelectable: Boolean,
) {
LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
) {
items(
sources,
key = { it.uid }
) { source ->
BundleItem(
bundle = source,
onDelete = {
onDelete(source)
},
onUpdate = {
onUpdate(source)
},
selectable = bundlesSelectable,
onSelect = {
selectedSources.add(source)
},
isBundleSelected = selectedSources.contains(source),
toggleSelection = { bundleIsNotSelected ->
if (bundleIsNotSelected) {
selectedSources.add(source)
} else {
selectedSources.remove(source)
}
}
)
}
}
}

View File

@ -31,7 +31,6 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.component.AvailableUpdateDialog import app.revanced.manager.ui.component.AvailableUpdateDialog
import app.revanced.manager.ui.component.NotificationCard import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.component.bundle.BundleItem
import app.revanced.manager.ui.component.bundle.BundleTopBar import app.revanced.manager.ui.component.bundle.BundleTopBar
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticTab import app.revanced.manager.ui.component.haptics.HapticTab
@ -282,36 +281,20 @@ fun DashboardScreen(
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList()) val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
Column( BundleListScreen(
modifier = Modifier.fillMaxSize(),
) {
sources.forEach {
BundleItem(
bundle = it,
onDelete = { onDelete = {
vm.delete(it) vm.delete(it)
}, },
onUpdate = { onUpdate = {
vm.update(it) vm.update(it)
}, },
selectable = bundlesSelectable, sources = sources,
onSelect = { selectedSources = vm.selectedSources,
vm.selectedSources.add(it) bundlesSelectable = bundlesSelectable
},
isBundleSelected = vm.selectedSources.contains(it),
toggleSelection = { bundleIsNotSelected ->
if (bundleIsNotSelected) {
vm.selectedSources.add(it)
} else {
vm.selectedSources.remove(it)
}
}
) )
} }
} }
} }
}
}
) )
} }
} }

View File

@ -5,12 +5,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -19,11 +15,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.settings.Changelog import app.revanced.manager.ui.component.settings.Changelog
import app.revanced.manager.ui.viewmodel.ChangelogsViewModel import app.revanced.manager.ui.viewmodel.ChangelogsViewModel
import app.revanced.manager.util.formatNumber
import app.revanced.manager.util.relativeTime import app.revanced.manager.util.relativeTime
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@ -33,8 +28,6 @@ fun ChangelogsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: ChangelogsViewModel = koinViewModel() vm: ChangelogsViewModel = koinViewModel()
) { ) {
val changelogs = vm.changelogs
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
@ -43,54 +36,22 @@ fun ChangelogsScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
LazyColumnWithScrollbar( ColumnWithScrollbar(
modifier = Modifier modifier = Modifier
.padding(paddingValues) .padding(paddingValues)
.fillMaxSize(), .fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = if (changelogs.isNullOrEmpty()) Arrangement.Center else Arrangement.Top verticalArrangement = if (vm.releaseInfo == null) Arrangement.Center else Arrangement.Top
) { ) {
if (changelogs == null) { vm.releaseInfo?.let { info ->
item {
LoadingIndicator()
}
} else if (changelogs.isEmpty()) {
item {
Text(
text = stringResource(id = R.string.no_changelogs_found),
style = MaterialTheme.typography.titleLarge
)
}
} else {
val lastChangelog = changelogs.last()
items(
changelogs,
key = { it.version }
) { changelog ->
ChangelogItem(changelog, lastChangelog)
}
}
}
}
}
@Composable
fun ChangelogItem(
changelog: ChangelogsViewModel.Changelog,
lastChangelog: ChangelogsViewModel.Changelog
) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Changelog( Changelog(
markdown = changelog.body.replace("`", ""), markdown = info.description.replace("`", ""),
version = changelog.version, version = info.version,
downloadCount = changelog.downloadCount.formatNumber(), publishDate = info.createdAt.relativeTime(LocalContext.current)
publishDate = changelog.publishDate.relativeTime(LocalContext.current)
)
if (changelog != lastChangelog) {
HorizontalDivider(
modifier = Modifier.padding(top = 32.dp),
color = MaterialTheme.colorScheme.outlineVariant
) )
} }
} ?: LoadingIndicator()
}
} }
} }

View File

@ -33,12 +33,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.BuildConfig import app.revanced.manager.BuildConfig
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.settings.Changelog import app.revanced.manager.ui.component.settings.Changelog
import app.revanced.manager.ui.viewmodel.UpdateViewModel import app.revanced.manager.ui.viewmodel.UpdateViewModel
import app.revanced.manager.ui.viewmodel.UpdateViewModel.Changelog
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
import app.revanced.manager.util.formatNumber
import app.revanced.manager.util.relativeTime import app.revanced.manager.util.relativeTime
import com.gigamole.composefadingedges.content.FadingEdgesContentType import com.gigamole.composefadingedges.content.FadingEdgesContentType
import com.gigamole.composefadingedges.content.scrollconfig.FadingEdgesScrollConfig import com.gigamole.composefadingedges.content.scrollconfig.FadingEdgesScrollConfig
@ -77,10 +76,10 @@ fun UpdateScreen(
) { ) {
Header( Header(
vm.state, vm.state,
vm.changelog, vm.releaseInfo,
DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize) DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize)
) )
vm.changelog?.let { changelog -> vm.releaseInfo?.let { changelog ->
HorizontalDivider() HorizontalDivider()
Changelog(changelog) Changelog(changelog)
} ?: Spacer(modifier = Modifier.weight(1f)) } ?: Spacer(modifier = Modifier.weight(1f))
@ -118,7 +117,7 @@ private fun MeteredDownloadConfirmationDialog(
} }
@Composable @Composable
private fun Header(state: State, changelog: Changelog?, downloadData: DownloadData) { private fun Header(state: State, releaseInfo: ReVancedAsset?, downloadData: DownloadData) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text( Text(
text = stringResource(state.title), text = stringResource(state.title),
@ -134,11 +133,11 @@ private fun Header(state: State, changelog: Changelog?, downloadData: DownloadDa
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
changelog?.let { changelog -> releaseInfo?.version?.let {
Text( Text(
text = stringResource( text = stringResource(
id = R.string.new_version, R.string.new_version,
changelog.version.replace("v", "") it.replace("v", "")
), ),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
@ -170,7 +169,7 @@ private fun Header(state: State, changelog: Changelog?, downloadData: DownloadDa
} }
@Composable @Composable
private fun ColumnScope.Changelog(changelog: Changelog) { private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column( Column(
modifier = Modifier modifier = Modifier
@ -194,10 +193,9 @@ private fun ColumnScope.Changelog(changelog: Changelog) {
) )
) { ) {
Changelog( Changelog(
markdown = changelog.body.replace("`", ""), markdown = releaseInfo.description.replace("`", ""),
version = changelog.version, version = releaseInfo.version,
downloadCount = changelog.downloadCount.formatNumber(), publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current)
publishDate = changelog.publishDate.relativeTime(LocalContext.current)
) )
} }
} }

View File

@ -8,9 +8,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.utils.getOrNull import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -18,27 +17,14 @@ class ChangelogsViewModel(
private val api: ReVancedAPI, private val api: ReVancedAPI,
private val app: Application, private val app: Application,
) : ViewModel() { ) : ViewModel() {
var changelogs: List<Changelog>? by mutableStateOf(null) var releaseInfo: ReVancedAsset? by mutableStateOf(null)
private set
init { init {
viewModelScope.launch { viewModelScope.launch {
uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") { uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") {
changelogs = api.getReleases("revanced-manager").getOrNull().orEmpty().map { release -> releaseInfo = api.getLatestAppInfo().getOrThrow()
Changelog(
release.version,
release.findAssetByType(APK_MIMETYPE).downloadCount,
release.metadata.publishedAt,
release.metadata.body
)
} }
} }
} }
}
data class Changelog(
val version: String,
val downloadCount: Int,
val publishDate: String,
val body: String,
)
} }

View File

@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.util.Log
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -19,16 +18,10 @@ import app.revanced.manager.R
import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.platform.NetworkInfo import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.dto.ReVancedRelease
import app.revanced.manager.network.service.HttpService import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.service.InstallService import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService
import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.simpleMessage
import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
import io.ktor.client.plugins.onDownload import io.ktor.client.plugins.onDownload
@ -38,7 +31,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File
class UpdateViewModel( class UpdateViewModel(
private val downloadOnScreenEntry: Boolean private val downloadOnScreenEntry: Boolean
@ -65,23 +57,14 @@ class UpdateViewModel(
var installError by mutableStateOf("") var installError by mutableStateOf("")
var changelog: Changelog? by mutableStateOf(null) var releaseInfo: ReVancedAsset? by mutableStateOf(null)
private set
private val location = fs.tempDir.resolve("updater.apk") private val location = fs.tempDir.resolve("updater.apk")
private var release: ReVancedRelease? = null
private val job = viewModelScope.launch { private val job = viewModelScope.launch {
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") { uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
withContext(Dispatchers.IO) { releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
val response = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
release = response
changelog = Changelog(
response.version,
response.findAssetByType(APK_MIMETYPE).downloadCount,
response.metadata.publishedAt,
response.metadata.body
)
}
if (downloadOnScreenEntry) { if (downloadOnScreenEntry) {
downloadUpdate() downloadUpdate()
} else { } else {
@ -92,16 +75,15 @@ class UpdateViewModel(
fun downloadUpdate(ignoreInternetCheck: Boolean = false) = viewModelScope.launch { fun downloadUpdate(ignoreInternetCheck: Boolean = false) = viewModelScope.launch {
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") { uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
val release = releaseInfo!!
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (!networkInfo.isSafe() && !ignoreInternetCheck) { if (!networkInfo.isSafe() && !ignoreInternetCheck) {
showInternetCheckDialog = true showInternetCheckDialog = true
} else { } else {
state = State.DOWNLOADING state = State.DOWNLOADING
val asset = release?.findAssetByType(APK_MIMETYPE)
?: throw Exception("couldn't find asset to download")
http.download(location) { http.download(location) {
url(asset.downloadUrl) url(release.downloadUrl)
onDownload { bytesSentTotal, contentLength -> onDownload { bytesSentTotal, contentLength ->
downloadedSize = bytesSentTotal downloadedSize = bytesSentTotal
totalSize = contentLength totalSize = contentLength
@ -153,13 +135,6 @@ class UpdateViewModel(
location.delete() location.delete()
} }
data class Changelog(
val version: String,
val downloadCount: Int,
val publishDate: String,
val body: String,
)
enum class State(@StringRes val title: Int, val showCancel: Boolean = false) { enum class State(@StringRes val title: Int, val showCancel: Boolean = false) {
CAN_DOWNLOAD(R.string.update_available), CAN_DOWNLOAD(R.string.update_available),
DOWNLOADING(R.string.downloading_manager_update, true), DOWNLOADING(R.string.downloading_manager_update, true),

View File

@ -3,11 +3,6 @@ package app.revanced.manager.util
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.icu.number.Notation
import android.icu.number.NumberFormatter
import android.icu.number.Precision
import android.icu.text.CompactDecimalFormat
import android.os.Build
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
@ -45,11 +40,13 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Duration import kotlinx.datetime.Clock
import java.time.ZoneId import kotlinx.datetime.LocalDateTime
import java.time.ZonedDateTime import kotlinx.datetime.TimeZone
import java.time.format.DateTimeFormatter import kotlinx.datetime.format.MonthNames
import java.time.format.DateTimeParseException import kotlinx.datetime.format.char
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import java.util.Locale import java.util.Locale
typealias PatchSelection = Map<Int, Set<String>> typealias PatchSelection = Map<Int, Set<String>>
@ -139,60 +136,39 @@ suspend fun <T> Flow<Iterable<T>>.collectEach(block: suspend (T) -> Unit) {
} }
} }
fun Int.formatNumber(): String { fun LocalDateTime.relativeTime(context: Context): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
NumberFormatter.with()
.notation(Notation.compactShort())
.decimal(NumberFormatter.DecimalSeparatorDisplay.ALWAYS)
.precision(Precision.fixedFraction(1))
.locale(Locale.getDefault())
.format(this)
.toString()
} else {
val compact = CompactDecimalFormat.getInstance(
Locale.getDefault(), CompactDecimalFormat.CompactStyle.SHORT
)
compact.maximumFractionDigits = 1
compact.format(this)
}
}
fun String.relativeTime(context: Context): String {
try { try {
val currentTime = ZonedDateTime.now(ZoneId.of("UTC")) val now = Clock.System.now()
val inputDateTime = ZonedDateTime.parse(this) val duration = now - this.toInstant(TimeZone.UTC)
val duration = Duration.between(inputDateTime, currentTime)
return when { return when {
duration.toMinutes() < 1 -> context.getString(R.string.just_now) duration.inWholeMinutes < 1 -> context.getString(R.string.just_now)
duration.toMinutes() < 60 -> context.getString( duration.inWholeMinutes < 60 -> context.getString(
R.string.minutes_ago, R.string.minutes_ago,
duration.toMinutes().toString() duration.inWholeMinutes.toString()
) )
duration.toHours() < 24 -> context.getString( duration.inWholeHours < 24 -> context.getString(
R.string.hours_ago, R.string.hours_ago,
duration.toHours().toString() duration.inWholeHours.toString()
) )
duration.toDays() < 30 -> context.getString( duration.inWholeHours < 30 -> context.getString(
R.string.days_ago, R.string.days_ago,
duration.toDays().toString() duration.inWholeDays.toString()
) )
else -> { else -> LocalDateTime.Format {
val formatter = DateTimeFormatter.ofPattern("MMM d") monthName(MonthNames.ENGLISH_ABBREVIATED)
val formattedDate = inputDateTime.format(formatter) char(' ')
if (inputDateTime.year != currentTime.year) { dayOfMonth()
val yearFormatter = DateTimeFormatter.ofPattern(", yyyy") if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) {
val formattedYear = inputDateTime.format(yearFormatter) chars(", ")
"$formattedDate$formattedYear" year()
} else {
formattedDate
} }
}.format(this)
} }
} } catch (e: IllegalArgumentException) {
} catch (e: DateTimeParseException) {
return context.getString(R.string.invalid_date) return context.getString(R.string.invalid_date)
} }
} }

View File

@ -14,6 +14,7 @@ placeholder = "1.1.2"
reorderable = "1.5.2" reorderable = "1.5.2"
serialization = "1.7.3" serialization = "1.7.3"
collection = "0.3.8" collection = "0.3.8"
datetime = "0.6.0"
room-version = "2.6.1" room-version = "2.6.1"
revanced-patcher = "21.0.0" revanced-patcher = "21.0.0"
revanced-library = "3.0.2" revanced-library = "3.0.2"
@ -72,6 +73,7 @@ placeholder-material3 = { group = "io.github.fornewid", name = "placeholder-mate
# Kotlinx # Kotlinx
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
kotlinx-collection-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collection" } kotlinx-collection-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collection" }
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "datetime" }
# Room # Room
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room-version" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room-version" }

View File

@ -15,7 +15,7 @@ dependencyResolutionManagement {
maven("https://jitpack.io") maven("https://jitpack.io")
mavenLocal() mavenLocal()
maven { maven {
// A repository must be speficied for some reason. "registry" is a dummy. // A repository must be specified for some reason. "registry" is a dummy.
url = uri("https://maven.pkg.github.com/revanced/registry") url = uri("https://maven.pkg.github.com/revanced/registry")
credentials { credentials {
username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String? username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String?