feat: backend

This commit is contained in:
CnC-Robert 2023-03-18 11:53:25 +01:00
parent 52bdb1cd6a
commit e5d898f025
24 changed files with 804 additions and 18 deletions

View File

@ -2,13 +2,22 @@ plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-parcelize")
kotlin("plugin.serialization") version "1.7.20"
}
repositories {
mavenCentral()
maven("https://jitpack.io")
google()
maven {
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials {
username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
}
}
}
android {
namespace = "app.revanced.manager.compose"
compileSdk = 33
@ -40,24 +49,39 @@ android {
buildFeatures.compose = true
composeOptions.kotlinCompilerExtensionVersion = "1.3.2"
composeOptions.kotlinCompilerExtensionVersion = "1.4.0"
}
dependencies {
// AndroidX Core
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.0")
implementation("androidx.core:core-splashscreen:1.0.0")
implementation("androidx.activity:activity-compose:1.6.1")
// Compose
val composeVersion = "1.3.3"
val composeVersion = "1.4.0-alpha05"
implementation("androidx.compose.ui:ui:$composeVersion")
implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion")
// Accompanist
val accompanistVersion = "0.29.1-alpha"
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
//implementation("com.google.accompanist:accompanist-placeholder-material:$accompanistVersion")
implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
//implementation("com.google.accompanist:accompanist-flowlayout:$accompanistVersion")
//implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion")
// KotlinX
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
// Material 3
implementation("androidx.compose.material3:material3:1.0.1")
implementation("androidx.compose.material3:material3:1.1.0-alpha08")
// ReVanced
implementation("app.revanced:revanced-patcher:6.4.3")
// Koin
implementation("io.insert-koin:koin-android:3.3.2")
@ -65,4 +89,12 @@ dependencies {
// Compose Navigation
implementation("dev.olshevski.navigation:reimagined:1.3.1")
// Ktor
val ktorVersion = "2.1.3"
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
}

View File

@ -2,6 +2,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="ReservedSystemPermission" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<application
android:name=".ManagerApplication"
android:allowBackup="true"
@ -13,16 +26,19 @@
android:supportsRtl="true"
android:theme="@style/Theme.ReVancedManager"
tools:targetApi="33">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.ReVancedManager">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<service android:name=".installer.service.InstallService" />
<service android:name=".installer.service.UninstallService" />
</application>
</manifest>

View File

@ -4,14 +4,17 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme
import app.revanced.manager.compose.domain.manager.PreferencesManager
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.compose.destination.Destination
import app.revanced.manager.compose.ui.theme.ReVancedManagerTheme
import dev.olshevski.navigation.reimagined.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.rememberNavController
import app.revanced.manager.compose.ui.theme.Theme
import dev.olshevski.navigation.reimagined.*
import org.koin.android.ext.android.inject
class MainActivity : ComponentActivity() {
private val prefs: PreferencesManager by inject()
@ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
@ -20,8 +23,8 @@ class MainActivity : ComponentActivity() {
installSplashScreen()
setContent {
ReVancedManagerTheme(
darkTheme = true, // TODO: Implement preferences
dynamicColor = false
darkTheme = prefs.theme == Theme.SYSTEM && isSystemInDarkTheme() || prefs.theme == Theme.DARK,
dynamicColor = prefs.dynamicColor
) {
val navController = rememberNavController<Destination>(startDestination = Destination.Home)

View File

@ -1,6 +1,7 @@
package app.revanced.manager.compose
import android.app.Application
import app.revanced.manager.compose.di.*
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
@ -10,7 +11,13 @@ class ManagerApplication: Application() {
startKoin {
androidContext(this@ManagerApplication)
modules(emptyList()) // TODO: Add modules
modules(
httpModule,
preferencesModule,
repositoryModule,
serviceModule,
viewModelModule
)
}
}
}

View File

@ -0,0 +1,51 @@
package app.revanced.manager.compose.di
import android.content.Context
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import okhttp3.Cache
import okhttp3.Dns
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import java.net.Inet4Address
import java.net.InetAddress
val httpModule = module {
fun provideHttpClient(context: Context, json: Json) = HttpClient(OkHttp) {
engine {
config {
dns(object : Dns {
override fun lookup(hostname: String): List<InetAddress> {
val addresses = Dns.SYSTEM.lookup(hostname)
return if (hostname == "raw.githubusercontent.com") {
addresses.filterIsInstance<Inet4Address>()
} else {
addresses
}
}
})
cache(Cache(context.cacheDir.resolve("cache").also { it.mkdirs() }, 1024 * 1024 * 100))
followRedirects(true)
followSslRedirects(true)
}
}
install(ContentNegotiation) {
json(json)
}
}
fun provideJson() = Json {
encodeDefaults = true
isLenient = true
ignoreUnknownKeys = true
}
single {
provideHttpClient(androidContext(), get())
}
singleOf(::provideJson)
}

View File

@ -0,0 +1,14 @@
package app.revanced.manager.compose.di
import android.content.Context
import app.revanced.manager.compose.domain.manager.PreferencesManager
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val preferencesModule = module {
fun providePreferences(
context: Context
) = PreferencesManager(context.getSharedPreferences("preferences", Context.MODE_PRIVATE))
singleOf(::providePreferences)
}

View File

@ -0,0 +1,11 @@
package app.revanced.manager.compose.di
import app.revanced.manager.compose.domain.repository.ReVancedRepositoryImpl
import app.revanced.manager.compose.network.api.ManagerAPI
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val repositoryModule = module {
singleOf(::ReVancedRepositoryImpl)
singleOf(::ManagerAPI)
}

View File

@ -0,0 +1,20 @@
package app.revanced.manager.compose.di
import app.revanced.manager.compose.network.service.HttpService
import app.revanced.manager.compose.network.service.ReVancedService
import app.revanced.manager.compose.network.service.ReVancedServiceImpl
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val serviceModule = module {
fun provideReVancedService(
client: HttpService,
): ReVancedService {
return ReVancedServiceImpl(
client = client,
)
}
single { provideReVancedService(get()) }
singleOf(::HttpService)
}

View File

@ -0,0 +1,16 @@
package app.revanced.manager.compose.domain.manager
import android.content.SharedPreferences
import app.revanced.manager.compose.domain.manager.base.BasePreferenceManager
import app.revanced.manager.compose.ui.theme.Theme
/**
* @author Hyperion Authors, zt64
*/
class PreferencesManager(
sharedPreferences: SharedPreferences
) : BasePreferenceManager(sharedPreferences) {
var dynamicColor by booleanPreference("dynamic_color", true)
var theme by enumPreference("theme", Theme.SYSTEM)
//var sentry by booleanPreference("sentry", true)
}

View File

@ -0,0 +1,98 @@
package app.revanced.manager.compose.domain.manager.base
import android.content.SharedPreferences
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.edit
import kotlin.reflect.KProperty
/**
* @author Hyperion Authors, zt64
*/
abstract class BasePreferenceManager(
private val prefs: SharedPreferences
) {
protected fun getString(key: String, defaultValue: String?) =
prefs.getString(key, defaultValue)!!
private fun getBoolean(key: String, defaultValue: Boolean) = prefs.getBoolean(key, defaultValue)
private fun getInt(key: String, defaultValue: Int) = prefs.getInt(key, defaultValue)
private fun getFloat(key: String, defaultValue: Float) = prefs.getFloat(key, defaultValue)
protected inline fun <reified E : Enum<E>> getEnum(key: String, defaultValue: E) =
enumValueOf<E>(getString(key, defaultValue.name))
protected fun putString(key: String, value: String?) = prefs.edit { putString(key, value) }
private fun putBoolean(key: String, value: Boolean) = prefs.edit { putBoolean(key, value) }
private fun putInt(key: String, value: Int) = prefs.edit { putInt(key, value) }
private fun putFloat(key: String, value: Float) = prefs.edit { putFloat(key, value) }
protected inline fun <reified E : Enum<E>> putEnum(key: String, value: E) =
putString(key, value.name)
protected class Preference<T>(
private val key: String,
defaultValue: T,
getter: (key: String, defaultValue: T) -> T,
private val setter: (key: String, newValue: T) -> Unit
) {
@Suppress("RedundantSetter")
var value by mutableStateOf(getter(key, defaultValue))
private set
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
value = newValue
setter(key, newValue)
}
}
protected fun stringPreference(
key: String,
defaultValue: String?
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getString,
setter = ::putString
)
protected fun booleanPreference(
key: String,
defaultValue: Boolean
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getBoolean,
setter = ::putBoolean
)
protected fun intPreference(
key: String,
defaultValue: Int
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getInt,
setter = ::putInt
)
protected fun floatPreference(
key: String,
defaultValue: Float
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getFloat,
setter = ::putFloat
)
protected inline fun <reified E : Enum<E>> enumPreference(
key: String,
defaultValue: E
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getEnum,
setter = ::putEnum
)
}

View File

@ -0,0 +1,25 @@
package app.revanced.manager.compose.domain.repository
import app.revanced.manager.compose.network.api.PatchesAsset
import app.revanced.manager.compose.network.dto.ReVancedReleases
import app.revanced.manager.compose.network.dto.ReVancedRepositories
import app.revanced.manager.compose.network.service.ReVancedService
import app.revanced.manager.compose.network.utils.APIResponse
interface ReVancedRepository {
suspend fun getAssets(): APIResponse<ReVancedReleases>
suspend fun getContributors(): APIResponse<ReVancedRepositories>
suspend fun findAsset(repo: String, file: String): PatchesAsset
}
class ReVancedRepositoryImpl(
private val service: ReVancedService
) : ReVancedRepository {
override suspend fun getAssets() = service.getAssets()
override suspend fun getContributors() = service.getContributors()
override suspend fun findAsset(repo: String, file: String) = service.findAsset(repo, file)
}

View File

@ -0,0 +1,47 @@
package app.revanced.manager.compose.installer.service
import android.app.Service
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import android.os.IBinder
class InstallService : Service() {
override fun onStartCommand(
intent: Intent, flags: Int, startId: Int
): Int {
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
when (extraStatus) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
startActivity(if (Build.VERSION.SDK_INT >= 33) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
} else {
intent.getParcelableExtra(Intent.EXTRA_INTENT)
}.apply {
this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
}
else -> {
sendBroadcast(Intent().apply {
action = APP_INSTALL_ACTION
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
})
}
}
stopSelf()
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
companion object {
const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
}
}

View File

@ -0,0 +1,42 @@
package app.revanced.manager.compose.installer.service
import android.app.Service
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import android.os.IBinder
class UninstallService : Service() {
override fun onStartCommand(
intent: Intent,
flags: Int,
startId: Int
): Int {
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
startActivity(if (Build.VERSION.SDK_INT >= 33) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
} else {
intent.getParcelableExtra(Intent.EXTRA_INTENT)
}.apply {
this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
}
else -> {
sendBroadcast(Intent().apply {
action = APP_UNINSTALL_ACTION
})
}
}
stopSelf()
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
companion object {
const val APP_UNINSTALL_ACTION = "APP_UNINSTALL_ACTION"
}
}

View File

@ -0,0 +1,68 @@
package app.revanced.manager.compose.installer.utils
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.os.Build
import app.revanced.manager.compose.installer.service.InstallService
import app.revanced.manager.compose.installer.service.UninstallService
import java.io.File
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
object PM {
fun installApp(apk: File, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
val session =
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
session.writeApk(apk)
session.commit(context.installIntentSender)
session.close()
}
fun uninstallPackage(pkg: String, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
packageInstaller.uninstall(pkg, context.uninstallIntentSender)
}
}
private fun PackageInstaller.Session.writeApk(apk: File) {
apk.inputStream().use { inputStream ->
openWrite(apk.name, 0, apk.length()).use { outputStream ->
inputStream.copyTo(outputStream, byteArraySize)
fsync(outputStream)
}
}
}
private val intentFlags
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
PendingIntent.FLAG_MUTABLE
else
0
private val sessionParams
get() = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL
).apply {
setInstallReason(PackageManager.INSTALL_REASON_USER)
}
private val Context.installIntentSender
get() = PendingIntent.getService(
this,
0,
Intent(this, InstallService::class.java),
intentFlags
).intentSender
private val Context.uninstallIntentSender
get() = PendingIntent.getService(
this,
0,
Intent(this, UninstallService::class.java),
intentFlags
).intentSender

View File

@ -0,0 +1,66 @@
package app.revanced.manager.compose.network.api
import android.app.Application
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import app.revanced.manager.compose.domain.repository.ReVancedRepositoryImpl
import app.revanced.manager.compose.util.ghIntegrations
import app.revanced.manager.compose.util.ghPatches
import app.revanced.manager.compose.util.tag
import app.revanced.manager.compose.util.toast
import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.util.cio.*
import io.ktor.utils.io.*
import java.io.File
class ManagerAPI(
private val app: Application,
private val client: HttpClient,
private val revancedRepository: ReVancedRepositoryImpl
) {
var downloadProgress: Float? by mutableStateOf(null)
private suspend fun downloadAsset(downloadUrl: String, saveLocation: File) {
client.get(downloadUrl) {
onDownload { bytesSentTotal, contentLength ->
downloadProgress = (bytesSentTotal.toFloat() / contentLength.toFloat())
}
}.bodyAsChannel().copyAndClose(saveLocation.writeChannel())
downloadProgress = null
}
suspend fun downloadPatchBundle() {
try {
val downloadUrl = revancedRepository.findAsset(ghPatches, ".jar").downloadUrl
val patchesFile = app.filesDir.resolve("patch-bundles").also { it.mkdirs() }
.resolve("patchbundle.jar")
downloadAsset(downloadUrl, patchesFile)
} catch (e: Exception) {
Log.e(tag, "Failed to download patch bundle", e)
app.toast("Failed to download patch bundle")
}
}
suspend fun downloadIntegrations() {
try {
val downloadUrl = revancedRepository.findAsset(ghIntegrations, ".apk").downloadUrl
val integrationsFile = app.filesDir.resolve("integrations").also { it.mkdirs() }
.resolve("integrations.apk")
downloadAsset(downloadUrl, integrationsFile)
} catch (e: Exception) {
Log.e(tag, "Failed to download integrations", e)
app.toast("Failed to download integrations")
}
}
}
data class PatchesAsset(
val downloadUrl: String, val name: String
)
class MissingAssetException : Exception()

View File

@ -0,0 +1,21 @@
package app.revanced.manager.compose.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class ReVancedRepositories(
@SerialName("repositories") val repositories: List<ReVancedRepository>,
)
@Serializable
class ReVancedRepository(
@SerialName("name") val name: String,
@SerialName("contributors") val contributors: List<ReVancedContributor>,
)
@Serializable
class ReVancedContributor(
@SerialName("login") val username: String,
@SerialName("avatar_url") val avatarUrl: String,
)

View File

@ -0,0 +1,20 @@
package app.revanced.manager.compose.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class ReVancedReleases(
@SerialName("tools") val tools: List<Assets>,
)
@Serializable
class Assets(
@SerialName("repository") val repository: String,
@SerialName("version") val version: String,
@SerialName("timestamp") val timestamp: String,
@SerialName("name") val name: String,
@SerialName("size") val size: String?,
@SerialName("browser_download_url") val downloadUrl: String,
@SerialName("content_type") val content_type: String
)

View File

@ -0,0 +1,51 @@
package app.revanced.manager.compose.network.service
import android.util.Log
import app.revanced.manager.compose.network.utils.APIError
import app.revanced.manager.compose.network.utils.APIFailure
import app.revanced.manager.compose.network.utils.APIResponse
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
/**
* @author Aliucord Authors, DiamondMiner88
*/
class HttpService(
val json: Json,
val http: HttpClient,
) {
suspend inline fun <reified T> request(builder: HttpRequestBuilder.() -> Unit = {}): APIResponse<T> {
var body: String? = null
val response = try {
val response = http.request(builder)
if (response.status.isSuccess()) {
body = response.bodyAsText()
if (T::class == String::class) {
return APIResponse.Success(body as T)
}
APIResponse.Success(json.decodeFromString<T>(body))
} else {
body = try {
response.bodyAsText()
} catch (t: Throwable) {
null
}
Log.e("ReVanced Manager", "Failed to fetch: API error, http status: ${response.status}, body: $body")
APIResponse.Error(APIError(response.status, body))
}
} catch (t: Throwable) {
Log.e("ReVanced Manager", "Failed to fetch: error: $t, body: $body")
APIResponse.Failure(APIFailure(t, body))
}
return response
}
}

View File

@ -0,0 +1,52 @@
package app.revanced.manager.compose.network.service
import app.revanced.manager.compose.network.api.MissingAssetException
import app.revanced.manager.compose.network.api.PatchesAsset
import app.revanced.manager.compose.network.dto.ReVancedReleases
import app.revanced.manager.compose.network.dto.ReVancedRepositories
import app.revanced.manager.compose.network.utils.APIResponse
import app.revanced.manager.compose.network.utils.getOrNull
import app.revanced.manager.compose.util.apiURL
import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
interface ReVancedService {
suspend fun getAssets(): APIResponse<ReVancedReleases>
suspend fun getContributors(): APIResponse<ReVancedRepositories>
suspend fun findAsset(repo: String, file: String): PatchesAsset
}
class ReVancedServiceImpl(
private val client: HttpService,
) : ReVancedService {
override suspend fun getAssets(): APIResponse<ReVancedReleases> {
return withContext(Dispatchers.IO) {
client.request {
url("$apiUrl/tools")
}
}
}
override suspend fun getContributors(): APIResponse<ReVancedRepositories> {
return withContext(Dispatchers.IO) {
client.request {
url("$apiUrl/contributors")
}
}
}
override suspend fun findAsset(repo: String, file: String): PatchesAsset {
val releases = getAssets().getOrNull() ?: throw Exception("Cannot retrieve assets")
val asset = releases.tools.find { asset ->
(asset.name.contains(file) && asset.repository.contains(repo))
} ?: throw MissingAssetException()
return PatchesAsset(asset.downloadUrl, asset.name)
}
private companion object {
private const val apiUrl = apiURL
}
}

View File

@ -0,0 +1,86 @@
@file:Suppress("NOTHING_TO_INLINE")
package app.revanced.manager.compose.network.utils
import io.ktor.http.*
/**
* @author Aliucord Authors, DiamondMiner88
*/
sealed interface APIResponse<T> {
data class Success<T>(val data: T) : APIResponse<T>
data class Error<T>(val error: APIError) : APIResponse<T>
data class Failure<T>(val error: APIFailure) : APIResponse<T>
}
class APIError(code: HttpStatusCode, body: String?) : Error("HTTP Code $code, Body: $body")
class APIFailure(error: Throwable, body: String?) : Error(body, error)
inline fun <T, R> APIResponse<T>.fold(
success: (T) -> R,
error: (APIError) -> R,
failure: (APIFailure) -> R
): R {
return when (this) {
is APIResponse.Success -> success(this.data)
is APIResponse.Error -> error(this.error)
is APIResponse.Failure -> failure(this.error)
}
}
inline fun <T, R> APIResponse<T>.fold(
success: (T) -> R,
fail: (Error) -> R,
): R {
return when (this) {
is APIResponse.Success -> success(data)
is APIResponse.Error -> fail(error)
is APIResponse.Failure -> fail(error)
}
}
@Suppress("UNCHECKED_CAST")
inline fun <T, R> APIResponse<T>.transform(block: (T) -> R): APIResponse<R> {
return if (this !is APIResponse.Success) {
// Error and Failure do not use the generic value
this as APIResponse<R>
} else {
APIResponse.Success(block(data))
}
}
inline fun <T> APIResponse<T>.getOrThrow(): T {
return fold(
success = { it },
fail = { throw it }
)
}
inline fun <T> APIResponse<T>.getOrNull(): T? {
return fold(
success = { it },
fail = { null }
)
}
@Suppress("UNCHECKED_CAST")
inline fun <T, R> APIResponse<T>.chain(block: (T) -> APIResponse<R>): APIResponse<R> {
return if (this !is APIResponse.Success) {
// Error and Failure do not use the generic value
this as APIResponse<R>
} else {
block(data)
}
}
@Suppress("UNCHECKED_CAST")
inline fun <T, R> APIResponse<T>.chain(secondary: APIResponse<R>): APIResponse<R> {
return if (secondary is APIResponse.Success) {
secondary
} else {
// Error and Failure do not use the generic value
this as APIResponse<R>
}
}

View File

@ -3,11 +3,7 @@ package app.revanced.manager.compose.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
@ -54,4 +50,10 @@ fun ReVancedManagerTheme(
typography = Typography,
content = content
)
}
enum class Theme(val displayName: String) {
SYSTEM("System"),
LIGHT("Light"),
DARK("Dark");
}

View File

@ -0,0 +1,11 @@
package app.revanced.manager.compose.util
private const val team = "revanced"
const val ghOrganization = "https://github.com/$team"
const val ghCli = "$team/revanced-cli"
const val ghPatches = "$team/revanced-patches"
const val ghPatcher = "$team/revanced-patcher"
const val ghManager = "$team/revanced-manager"
const val ghIntegrations = "$team/revanced-integrations"
const val tag = "ReVanced Manager"
const val apiURL = "https://releases.revanced.app"

View File

@ -0,0 +1,27 @@
package app.revanced.manager.compose.util
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager.NameNotFoundException
import android.graphics.drawable.Drawable
import android.widget.Toast
import androidx.core.net.toUri
fun Context.openUrl(url: String) {
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}
fun Context.loadIcon(string: String): Drawable? {
return try {
packageManager.getApplicationIcon(string)
} catch (e: NameNotFoundException) {
null
}
}
fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, string, duration).show()
}

View File

@ -7,7 +7,7 @@ buildscript {
plugins {
id("com.android.application") version "7.3.1" apply false
id("com.android.library") version "7.3.1" apply false
id("org.jetbrains.kotlin.android") version "1.7.20" apply false
id("org.jetbrains.kotlin.android") version "1.8.0" apply false
}
repositories {
google()