mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-04-29 21:44:26 +02:00
Merge branch 'compose-dev' into compose/downloader-system
This commit is contained in:
commit
d0cd29d625
@ -144,6 +144,7 @@ dependencies {
|
||||
// KotlinX
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.collection.immutable)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
// Room
|
||||
implementation(libs.room.runtime)
|
||||
|
@ -1,11 +1,9 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import app.revanced.manager.network.service.HttpService
|
||||
import app.revanced.manager.network.service.ReVancedService
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val serviceModule = module {
|
||||
singleOf(::ReVancedService)
|
||||
singleOf(::HttpService)
|
||||
}
|
@ -2,7 +2,7 @@ package app.revanced.manager.domain.bundles
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
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.utils.getOrThrow
|
||||
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) {
|
||||
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) {
|
||||
val (version, url) = info
|
||||
private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
|
||||
patchBundleOutputStream().use {
|
||||
http.streamTo(it) {
|
||||
url(url)
|
||||
url(info.downloadUrl)
|
||||
}
|
||||
}
|
||||
|
||||
saveVersion(version)
|
||||
saveVersion(info.version)
|
||||
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) :
|
||||
RemotePatchBundle(name, id, directory, endpoint) {
|
||||
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
|
||||
http.request<PatchBundleInfo> {
|
||||
http.request<ReVancedAsset> {
|
||||
url(endpoint)
|
||||
}.getOrThrow()
|
||||
}
|
||||
@ -68,10 +67,5 @@ class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
||||
RemotePatchBundle(name, id, directory, endpoint) {
|
||||
private val api: ReVancedAPI by inject()
|
||||
|
||||
override suspend fun getLatestInfo() = api
|
||||
.getLatestRelease("revanced-patches")
|
||||
.getOrThrow()
|
||||
.let {
|
||||
PatchBundleInfo(it.version, it.assets.first { it.name.endsWith(".rvp") }.downloadUrl)
|
||||
}
|
||||
override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow()
|
||||
}
|
@ -2,37 +2,41 @@ package app.revanced.manager.network.api
|
||||
|
||||
import android.os.Build
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.network.dto.ReVancedRelease
|
||||
import app.revanced.manager.network.service.ReVancedService
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
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.transform
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import io.ktor.client.request.url
|
||||
|
||||
class ReVancedAPI(
|
||||
private val service: ReVancedService,
|
||||
private val client: HttpService,
|
||||
private val prefs: PreferencesManager
|
||||
) {
|
||||
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) =
|
||||
service.getLatestRelease(apiUrl(), name).transform { it.release }
|
||||
|
||||
suspend fun getReleases(name: String) =
|
||||
service.getReleases(apiUrl(), name).transform { it.releases }
|
||||
private suspend inline fun <reified T> request(route: String) = request<T>(apiUrl(), route)
|
||||
|
||||
suspend fun getAppUpdate() =
|
||||
getLatestRelease("revanced-manager")
|
||||
.getOrThrow()
|
||||
.takeIf { it.version != Build.VERSION.RELEASE }
|
||||
getLatestAppInfo().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 {
|
||||
fun ReVancedRelease.findAssetByType(mime: String) =
|
||||
assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime)
|
||||
}
|
||||
}
|
||||
suspend fun getContributors() = request<List<ReVancedGitRepository>>("contributors")
|
||||
|
||||
class MissingAssetException(type: String) : Exception("No asset with type $type")
|
||||
suspend fun getInfo(api: String? = null) = request<ReVancedInfo>(api ?: apiUrl(), "about")
|
||||
}
|
@ -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,
|
||||
)
|
@ -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)
|
@ -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,
|
||||
)
|
||||
|
@ -3,19 +3,15 @@ package app.revanced.manager.network.dto
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ReVancedGitRepositories(
|
||||
val repositories: List<ReVancedGitRepository>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedGitRepository(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val contributors: List<ReVancedContributor>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedContributor(
|
||||
@SerialName("login") val username: String,
|
||||
@SerialName("name") val username: String,
|
||||
@SerialName("avatar_url") val avatarUrl: String,
|
||||
)
|
||||
)
|
@ -1,12 +1,8 @@
|
||||
package app.revanced.manager.network.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ReVancedInfoParent(
|
||||
val info: ReVancedInfo,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedInfo(
|
||||
val name: String,
|
||||
@ -43,7 +39,8 @@ data class ReVancedDonation(
|
||||
@Serializable
|
||||
data class ReVancedWallet(
|
||||
val network: String,
|
||||
val currency_code: String,
|
||||
@SerialName("currency_code")
|
||||
val currencyCode: String,
|
||||
val address: String,
|
||||
val preferred: Boolean
|
||||
)
|
||||
|
@ -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
|
||||
)
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -26,7 +26,6 @@ import app.revanced.manager.ui.component.Markdown
|
||||
fun Changelog(
|
||||
markdown: String,
|
||||
version: String,
|
||||
downloadCount: String,
|
||||
publishDate: String
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
@ -55,10 +54,6 @@ fun Changelog(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Tag(
|
||||
Icons.Outlined.FileDownload,
|
||||
downloadCount
|
||||
)
|
||||
Tag(
|
||||
Icons.Outlined.CalendarToday,
|
||||
publishDate
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -31,7 +31,6 @@ import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||
import app.revanced.manager.ui.component.AvailableUpdateDialog
|
||||
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.haptics.HapticFloatingActionButton
|
||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||
@ -282,33 +281,17 @@ fun DashboardScreen(
|
||||
|
||||
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
sources.forEach {
|
||||
BundleItem(
|
||||
bundle = it,
|
||||
onDelete = {
|
||||
vm.delete(it)
|
||||
},
|
||||
onUpdate = {
|
||||
vm.update(it)
|
||||
},
|
||||
selectable = bundlesSelectable,
|
||||
onSelect = {
|
||||
vm.selectedSources.add(it)
|
||||
},
|
||||
isBundleSelected = vm.selectedSources.contains(it),
|
||||
toggleSelection = { bundleIsNotSelected ->
|
||||
if (bundleIsNotSelected) {
|
||||
vm.selectedSources.add(it)
|
||||
} else {
|
||||
vm.selectedSources.remove(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
BundleListScreen(
|
||||
onDelete = {
|
||||
vm.delete(it)
|
||||
},
|
||||
onUpdate = {
|
||||
vm.update(it)
|
||||
},
|
||||
sources = sources,
|
||||
selectedSources = vm.selectedSources,
|
||||
bundlesSelectable = bundlesSelectable
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,8 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -19,11 +15,10 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
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.settings.Changelog
|
||||
import app.revanced.manager.ui.viewmodel.ChangelogsViewModel
|
||||
import app.revanced.manager.util.formatNumber
|
||||
import app.revanced.manager.util.relativeTime
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@ -33,8 +28,6 @@ fun ChangelogsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: ChangelogsViewModel = koinViewModel()
|
||||
) {
|
||||
val changelogs = vm.changelogs
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
@ -43,54 +36,22 @@ fun ChangelogsScreen(
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumnWithScrollbar(
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize(),
|
||||
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) {
|
||||
item {
|
||||
LoadingIndicator()
|
||||
}
|
||||
} else if (changelogs.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.no_changelogs_found),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
vm.releaseInfo?.let { info ->
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Changelog(
|
||||
markdown = info.description.replace("`", ""),
|
||||
version = info.version,
|
||||
publishDate = info.createdAt.relativeTime(LocalContext.current)
|
||||
)
|
||||
}
|
||||
} 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)) {
|
||||
Changelog(
|
||||
markdown = changelog.body.replace("`", ""),
|
||||
version = changelog.version,
|
||||
downloadCount = changelog.downloadCount.formatNumber(),
|
||||
publishDate = changelog.publishDate.relativeTime(LocalContext.current)
|
||||
)
|
||||
if (changelog != lastChangelog) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(top = 32.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
} ?: LoadingIndicator()
|
||||
}
|
||||
}
|
||||
}
|
@ -33,12 +33,11 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.BuildConfig
|
||||
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.settings.Changelog
|
||||
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.util.formatNumber
|
||||
import app.revanced.manager.util.relativeTime
|
||||
import com.gigamole.composefadingedges.content.FadingEdgesContentType
|
||||
import com.gigamole.composefadingedges.content.scrollconfig.FadingEdgesScrollConfig
|
||||
@ -77,10 +76,10 @@ fun UpdateScreen(
|
||||
) {
|
||||
Header(
|
||||
vm.state,
|
||||
vm.changelog,
|
||||
vm.releaseInfo,
|
||||
DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize)
|
||||
)
|
||||
vm.changelog?.let { changelog ->
|
||||
vm.releaseInfo?.let { changelog ->
|
||||
HorizontalDivider()
|
||||
Changelog(changelog)
|
||||
} ?: Spacer(modifier = Modifier.weight(1f))
|
||||
@ -118,7 +117,7 @@ private fun MeteredDownloadConfirmationDialog(
|
||||
}
|
||||
|
||||
@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)) {
|
||||
Text(
|
||||
text = stringResource(state.title),
|
||||
@ -134,11 +133,11 @@ private fun Header(state: State, changelog: Changelog?, downloadData: DownloadDa
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
changelog?.let { changelog ->
|
||||
releaseInfo?.version?.let {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.new_version,
|
||||
changelog.version.replace("v", "")
|
||||
R.string.new_version,
|
||||
it.replace("v", "")
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@ -170,7 +169,7 @@ private fun Header(state: State, changelog: Changelog?, downloadData: DownloadDa
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.Changelog(changelog: Changelog) {
|
||||
private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@ -194,10 +193,9 @@ private fun ColumnScope.Changelog(changelog: Changelog) {
|
||||
)
|
||||
) {
|
||||
Changelog(
|
||||
markdown = changelog.body.replace("`", ""),
|
||||
version = changelog.version,
|
||||
downloadCount = changelog.downloadCount.formatNumber(),
|
||||
publishDate = changelog.publishDate.relativeTime(LocalContext.current)
|
||||
markdown = releaseInfo.description.replace("`", ""),
|
||||
version = releaseInfo.version,
|
||||
publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -8,9 +8,8 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
|
||||
import app.revanced.manager.network.utils.getOrNull
|
||||
import app.revanced.manager.util.APK_MIMETYPE
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.utils.getOrThrow
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ -18,27 +17,14 @@ class ChangelogsViewModel(
|
||||
private val api: ReVancedAPI,
|
||||
private val app: Application,
|
||||
) : ViewModel() {
|
||||
var changelogs: List<Changelog>? by mutableStateOf(null)
|
||||
var releaseInfo: ReVancedAsset? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") {
|
||||
changelogs = api.getReleases("revanced-manager").getOrNull().orEmpty().map { release ->
|
||||
Changelog(
|
||||
release.version,
|
||||
release.findAssetByType(APK_MIMETYPE).downloadCount,
|
||||
release.metadata.publishedAt,
|
||||
release.metadata.body
|
||||
)
|
||||
}
|
||||
releaseInfo = api.getLatestAppInfo().getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Changelog(
|
||||
val version: String,
|
||||
val downloadCount: Int,
|
||||
val publishDate: String,
|
||||
val body: String,
|
||||
)
|
||||
}
|
@ -6,7 +6,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
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.NetworkInfo
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
|
||||
import app.revanced.manager.network.dto.ReVancedRelease
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
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.UninstallService
|
||||
import app.revanced.manager.util.APK_MIMETYPE
|
||||
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.uiSafe
|
||||
import io.ktor.client.plugins.onDownload
|
||||
@ -38,7 +31,6 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
|
||||
class UpdateViewModel(
|
||||
private val downloadOnScreenEntry: Boolean
|
||||
@ -65,23 +57,14 @@ class UpdateViewModel(
|
||||
|
||||
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 var release: ReVancedRelease? = null
|
||||
private val job = viewModelScope.launch {
|
||||
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||
withContext(Dispatchers.IO) {
|
||||
val response = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
||||
releaseInfo = 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) {
|
||||
downloadUpdate()
|
||||
} else {
|
||||
@ -92,16 +75,15 @@ class UpdateViewModel(
|
||||
|
||||
fun downloadUpdate(ignoreInternetCheck: Boolean = false) = viewModelScope.launch {
|
||||
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
|
||||
val release = releaseInfo!!
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!networkInfo.isSafe() && !ignoreInternetCheck) {
|
||||
showInternetCheckDialog = true
|
||||
} else {
|
||||
state = State.DOWNLOADING
|
||||
val asset = release?.findAssetByType(APK_MIMETYPE)
|
||||
?: throw Exception("couldn't find asset to download")
|
||||
|
||||
http.download(location) {
|
||||
url(asset.downloadUrl)
|
||||
url(release.downloadUrl)
|
||||
onDownload { bytesSentTotal, contentLength ->
|
||||
downloadedSize = bytesSentTotal
|
||||
totalSize = contentLength
|
||||
@ -153,13 +135,6 @@ class UpdateViewModel(
|
||||
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) {
|
||||
CAN_DOWNLOAD(R.string.update_available),
|
||||
DOWNLOADING(R.string.downloading_manager_update, true),
|
||||
|
@ -3,11 +3,6 @@ package app.revanced.manager.util
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
@ -45,11 +40,13 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Duration
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeParseException
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.format.MonthNames
|
||||
import kotlinx.datetime.format.char
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import java.util.Locale
|
||||
|
||||
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 {
|
||||
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 {
|
||||
fun LocalDateTime.relativeTime(context: Context): String {
|
||||
try {
|
||||
val currentTime = ZonedDateTime.now(ZoneId.of("UTC"))
|
||||
val inputDateTime = ZonedDateTime.parse(this)
|
||||
val duration = Duration.between(inputDateTime, currentTime)
|
||||
val now = Clock.System.now()
|
||||
val duration = now - this.toInstant(TimeZone.UTC)
|
||||
|
||||
return when {
|
||||
duration.toMinutes() < 1 -> context.getString(R.string.just_now)
|
||||
duration.toMinutes() < 60 -> context.getString(
|
||||
duration.inWholeMinutes < 1 -> context.getString(R.string.just_now)
|
||||
duration.inWholeMinutes < 60 -> context.getString(
|
||||
R.string.minutes_ago,
|
||||
duration.toMinutes().toString()
|
||||
duration.inWholeMinutes.toString()
|
||||
)
|
||||
|
||||
duration.toHours() < 24 -> context.getString(
|
||||
duration.inWholeHours < 24 -> context.getString(
|
||||
R.string.hours_ago,
|
||||
duration.toHours().toString()
|
||||
duration.inWholeHours.toString()
|
||||
)
|
||||
|
||||
duration.toDays() < 30 -> context.getString(
|
||||
duration.inWholeHours < 30 -> context.getString(
|
||||
R.string.days_ago,
|
||||
duration.toDays().toString()
|
||||
duration.inWholeDays.toString()
|
||||
)
|
||||
|
||||
else -> {
|
||||
val formatter = DateTimeFormatter.ofPattern("MMM d")
|
||||
val formattedDate = inputDateTime.format(formatter)
|
||||
if (inputDateTime.year != currentTime.year) {
|
||||
val yearFormatter = DateTimeFormatter.ofPattern(", yyyy")
|
||||
val formattedYear = inputDateTime.format(yearFormatter)
|
||||
"$formattedDate$formattedYear"
|
||||
} else {
|
||||
formattedDate
|
||||
else -> LocalDateTime.Format {
|
||||
monthName(MonthNames.ENGLISH_ABBREVIATED)
|
||||
char(' ')
|
||||
dayOfMonth()
|
||||
if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) {
|
||||
chars(", ")
|
||||
year()
|
||||
}
|
||||
}
|
||||
}.format(this)
|
||||
}
|
||||
} catch (e: DateTimeParseException) {
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return context.getString(R.string.invalid_date)
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ placeholder = "1.1.2"
|
||||
reorderable = "1.5.2"
|
||||
serialization = "1.7.3"
|
||||
collection = "0.3.8"
|
||||
datetime = "0.6.0"
|
||||
room-version = "2.6.1"
|
||||
revanced-patcher = "21.0.0"
|
||||
revanced-library = "3.0.2"
|
||||
@ -72,6 +73,7 @@ placeholder-material3 = { group = "io.github.fornewid", name = "placeholder-mate
|
||||
# Kotlinx
|
||||
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-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "datetime" }
|
||||
|
||||
# Room
|
||||
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room-version" }
|
||||
|
@ -15,7 +15,7 @@ dependencyResolutionManagement {
|
||||
maven("https://jitpack.io")
|
||||
mavenLocal()
|
||||
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")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String?
|
||||
|
Loading…
x
Reference in New Issue
Block a user