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
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.collection.immutable)
implementation(libs.kotlinx.datetime)
// Room
implementation(libs.room.runtime)

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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")
}

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.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,
)
)

View File

@ -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
)

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(
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

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.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
)
}
}
}

View File

@ -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()
}
}
}

View File

@ -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)
)
}
}

View File

@ -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,
)
}

View File

@ -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),

View File

@ -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)
}
}

View File

@ -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" }

View File

@ -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?