diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5c4f0e5b..493d7daf 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ef44dc1a..af437b32 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/MainActivity.kt b/app/src/main/java/app/revanced/manager/compose/MainActivity.kt
index d0500114..bdc05311 100644
--- a/app/src/main/java/app/revanced/manager/compose/MainActivity.kt
+++ b/app/src/main/java/app/revanced/manager/compose/MainActivity.kt
@@ -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(startDestination = Destination.Home)
diff --git a/app/src/main/java/app/revanced/manager/compose/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/compose/ManagerApplication.kt
index 5afdddc9..2a44586f 100644
--- a/app/src/main/java/app/revanced/manager/compose/ManagerApplication.kt
+++ b/app/src/main/java/app/revanced/manager/compose/ManagerApplication.kt
@@ -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
+ )
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/di/HttpModule.kt b/app/src/main/java/app/revanced/manager/compose/di/HttpModule.kt
new file mode 100644
index 00000000..c67a0369
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/di/HttpModule.kt
@@ -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 {
+ val addresses = Dns.SYSTEM.lookup(hostname)
+ return if (hostname == "raw.githubusercontent.com") {
+ addresses.filterIsInstance()
+ } 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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/di/PreferencesModule.kt b/app/src/main/java/app/revanced/manager/compose/di/PreferencesModule.kt
new file mode 100644
index 00000000..83c2887d
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/di/PreferencesModule.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/di/RepositoryModule.kt b/app/src/main/java/app/revanced/manager/compose/di/RepositoryModule.kt
new file mode 100644
index 00000000..1479824e
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/di/RepositoryModule.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/di/ServiceModule.kt b/app/src/main/java/app/revanced/manager/compose/di/ServiceModule.kt
new file mode 100644
index 00000000..3a08842d
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/di/ServiceModule.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/domain/manager/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/compose/domain/manager/PreferencesManager.kt
new file mode 100644
index 00000000..b84c647a
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/domain/manager/PreferencesManager.kt
@@ -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)
+}
diff --git a/app/src/main/java/app/revanced/manager/compose/domain/manager/base/BasePreferencesManager.kt b/app/src/main/java/app/revanced/manager/compose/domain/manager/base/BasePreferencesManager.kt
new file mode 100644
index 00000000..3fd68582
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/domain/manager/base/BasePreferencesManager.kt
@@ -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 > getEnum(key: String, defaultValue: E) =
+ enumValueOf(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 > putEnum(key: String, value: E) =
+ putString(key, value.name)
+
+ protected class Preference(
+ 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 > enumPreference(
+ key: String,
+ defaultValue: E
+ ) = Preference(
+ key = key,
+ defaultValue = defaultValue,
+ getter = ::getEnum,
+ setter = ::putEnum
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/domain/repository/ReVancedRepository.kt b/app/src/main/java/app/revanced/manager/compose/domain/repository/ReVancedRepository.kt
new file mode 100644
index 00000000..9e5aaaf7
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/domain/repository/ReVancedRepository.kt
@@ -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
+
+ suspend fun getContributors(): APIResponse
+
+ 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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/installer/service/InstallService.kt b/app/src/main/java/app/revanced/manager/compose/installer/service/InstallService.kt
new file mode 100644
index 00000000..2dea3242
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/installer/service/InstallService.kt
@@ -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"
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/installer/service/UninstallService.kt b/app/src/main/java/app/revanced/manager/compose/installer/service/UninstallService.kt
new file mode 100644
index 00000000..5780160b
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/installer/service/UninstallService.kt
@@ -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"
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/installer/utils/PM.kt b/app/src/main/java/app/revanced/manager/compose/installer/utils/PM.kt
new file mode 100644
index 00000000..aa84e45b
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/installer/utils/PM.kt
@@ -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
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/network/api/ManagerAPI.kt b/app/src/main/java/app/revanced/manager/compose/network/api/ManagerAPI.kt
new file mode 100644
index 00000000..ee89d26f
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/network/api/ManagerAPI.kt
@@ -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()
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/network/dto/ReVancedContributors.kt b/app/src/main/java/app/revanced/manager/compose/network/dto/ReVancedContributors.kt
new file mode 100644
index 00000000..5d125a4f
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/network/dto/ReVancedContributors.kt
@@ -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,
+)
+
+@Serializable
+class ReVancedRepository(
+ @SerialName("name") val name: String,
+ @SerialName("contributors") val contributors: List,
+)
+
+@Serializable
+class ReVancedContributor(
+ @SerialName("login") val username: String,
+ @SerialName("avatar_url") val avatarUrl: String,
+)
diff --git a/app/src/main/java/app/revanced/manager/compose/network/dto/ReVancedReleases.kt b/app/src/main/java/app/revanced/manager/compose/network/dto/ReVancedReleases.kt
new file mode 100644
index 00000000..e6b101c8
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/network/dto/ReVancedReleases.kt
@@ -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,
+)
+
+@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
+)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/network/service/HttpService.kt b/app/src/main/java/app/revanced/manager/compose/network/service/HttpService.kt
new file mode 100644
index 00000000..a17e1c6f
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/network/service/HttpService.kt
@@ -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 request(builder: HttpRequestBuilder.() -> Unit = {}): APIResponse {
+ 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(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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/network/service/ReVancedService.kt b/app/src/main/java/app/revanced/manager/compose/network/service/ReVancedService.kt
new file mode 100644
index 00000000..f7c3bf7d
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/network/service/ReVancedService.kt
@@ -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
+
+ suspend fun getContributors(): APIResponse
+
+ suspend fun findAsset(repo: String, file: String): PatchesAsset
+}
+
+class ReVancedServiceImpl(
+ private val client: HttpService,
+) : ReVancedService {
+ override suspend fun getAssets(): APIResponse {
+ return withContext(Dispatchers.IO) {
+ client.request {
+ url("$apiUrl/tools")
+ }
+ }
+ }
+
+ override suspend fun getContributors(): APIResponse {
+ 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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/network/utils/APIResponse.kt b/app/src/main/java/app/revanced/manager/compose/network/utils/APIResponse.kt
new file mode 100644
index 00000000..7c28e151
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/network/utils/APIResponse.kt
@@ -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 {
+ data class Success(val data: T) : APIResponse
+ data class Error(val error: APIError) : APIResponse
+ data class Failure(val error: APIFailure) : APIResponse
+}
+
+class APIError(code: HttpStatusCode, body: String?) : Error("HTTP Code $code, Body: $body")
+
+class APIFailure(error: Throwable, body: String?) : Error(body, error)
+
+inline fun APIResponse.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 APIResponse.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 APIResponse.transform(block: (T) -> R): APIResponse {
+ return if (this !is APIResponse.Success) {
+ // Error and Failure do not use the generic value
+ this as APIResponse
+ } else {
+ APIResponse.Success(block(data))
+ }
+}
+
+inline fun APIResponse.getOrThrow(): T {
+ return fold(
+ success = { it },
+ fail = { throw it }
+ )
+}
+
+inline fun APIResponse.getOrNull(): T? {
+ return fold(
+ success = { it },
+ fail = { null }
+ )
+}
+
+@Suppress("UNCHECKED_CAST")
+inline fun APIResponse.chain(block: (T) -> APIResponse): APIResponse {
+ return if (this !is APIResponse.Success) {
+ // Error and Failure do not use the generic value
+ this as APIResponse
+ } else {
+ block(data)
+ }
+}
+
+@Suppress("UNCHECKED_CAST")
+inline fun APIResponse.chain(secondary: APIResponse): APIResponse {
+ return if (secondary is APIResponse.Success) {
+ secondary
+ } else {
+ // Error and Failure do not use the generic value
+ this as APIResponse
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/ui/theme/Theme.kt b/app/src/main/java/app/revanced/manager/compose/ui/theme/Theme.kt
index dfa0ba6c..3e871786 100644
--- a/app/src/main/java/app/revanced/manager/compose/ui/theme/Theme.kt
+++ b/app/src/main/java/app/revanced/manager/compose/ui/theme/Theme.kt
@@ -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");
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/util/Constants.kt b/app/src/main/java/app/revanced/manager/compose/util/Constants.kt
new file mode 100644
index 00000000..f204fd6e
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/util/Constants.kt
@@ -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"
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/compose/util/Util.kt b/app/src/main/java/app/revanced/manager/compose/util/Util.kt
new file mode 100644
index 00000000..a20d0750
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/compose/util/Util.kt
@@ -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()
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 6264a09a..b71d0ff0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -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()