mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-18 00:33:58 +00:00
Compare commits
9 Commits
dev
...
feat/impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b28e9a15be | ||
|
|
9fc2b4fdef | ||
|
|
08662b2132 | ||
|
|
0169fd2109 | ||
|
|
c53d0462d6 | ||
|
|
af8f2afa36 | ||
|
|
9cdb8eafb3 | ||
|
|
fda0e1697b | ||
|
|
2d98923f50 |
43
.github/workflows/pull_strings.yml
vendored
43
.github/workflows/pull_strings.yml
vendored
@@ -1,43 +0,0 @@
|
|||||||
name: Pull strings
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * 0"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pull:
|
|
||||||
name: Pull strings
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
ref: dev
|
|
||||||
clean: true
|
|
||||||
|
|
||||||
- name: Pull strings
|
|
||||||
uses: crowdin/github-action@v2
|
|
||||||
with:
|
|
||||||
config: crowdin.yml
|
|
||||||
upload_sources: false
|
|
||||||
download_translations: true
|
|
||||||
skip_ref_checkout: true
|
|
||||||
localization_branch_name: feat/translations
|
|
||||||
create_pull_request: false
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
|
||||||
|
|
||||||
- name: Open pull request
|
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
uses: repo-sync/pull-request@v2
|
|
||||||
with:
|
|
||||||
source_branch: feat/translations
|
|
||||||
destination_branch: dev
|
|
||||||
pr_title: "chore: Sync translations"
|
|
||||||
pr_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
|
|
||||||
26
.github/workflows/push_strings.yml
vendored
26
.github/workflows/push_strings.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
name: Push strings
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
paths:
|
|
||||||
- app/src/main/res/values/strings.xml
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
push:
|
|
||||||
name: Push strings
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Push strings
|
|
||||||
uses: crowdin/github-action@v2
|
|
||||||
with:
|
|
||||||
config: crowdin.yml
|
|
||||||
upload_sources: true
|
|
||||||
env:
|
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
|
||||||
@@ -24,6 +24,14 @@ public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/
|
|||||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
|
||||||
|
public fun <init> ()V
|
||||||
|
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||||
|
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||||
|
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||||
|
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/Downloader {
|
public final class app/revanced/manager/plugin/downloader/Downloader {
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
}
|
}
|
||||||
@@ -77,6 +85,14 @@ public final class app/revanced/manager/plugin/downloader/Package : android/os/P
|
|||||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator {
|
||||||
|
public fun <init> ()V
|
||||||
|
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package;
|
||||||
|
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||||
|
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package;
|
||||||
|
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
|
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +159,14 @@ public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEve
|
|||||||
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
|
||||||
|
public fun <init> ()V
|
||||||
|
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
|
||||||
|
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||||
|
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
|
||||||
|
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
|
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
|
||||||
public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||||
public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.library)
|
alias(libs.plugins.android.library)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
@@ -19,16 +17,9 @@ dependencies {
|
|||||||
implementation(libs.appcompat)
|
implementation(libs.appcompat)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
|
||||||
jvmToolchain(17)
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget = JvmTarget.JVM_17
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.manager.plugin.downloader"
|
namespace = "app.revanced.manager.plugin.downloader"
|
||||||
compileSdk = 36
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
@@ -51,6 +42,10 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
aidl = true
|
aidl = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,3 @@
|
|||||||
# app [1.26.0-dev.20](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.19...v1.26.0-dev.20) (2026-01-09)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* Save FAB freaking out in select patches screen ([4c0b6b0](https://github.com/ReVanced/revanced-manager/commit/4c0b6b02e95a8d6f655bcf5c25493b1f9a4a4dcd))
|
|
||||||
|
|
||||||
# app [1.26.0-dev.19](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.18...v1.26.0-dev.19) (2026-01-08)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **locales:** use buildconfig instead of generating kt file ([72b1db9](https://github.com/ReVanced/revanced-manager/commit/72b1db9a2f33ab5d5fffd8ba83c05901eff19bea))
|
|
||||||
|
|
||||||
# app [1.26.0-dev.18](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.17...v1.26.0-dev.18) (2026-01-08)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* Prevent trailing comma when no locales are generated ([b16931c](https://github.com/ReVanced/revanced-manager/commit/b16931ca79d5ce4d17c75f6dd3bf6f976b8ff7be))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Add language settings ([#2913](https://github.com/ReVanced/revanced-manager/issues/2913)) ([df31b39](https://github.com/ReVanced/revanced-manager/commit/df31b39cc8c1fbf00bc3301468e8e7e4b283caf2))
|
|
||||||
|
|
||||||
# app [1.26.0-dev.17](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.16...v1.26.0-dev.17) (2026-01-06)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* allow updating patches on metered networks ([9d9a0e8](https://github.com/ReVanced/revanced-manager/commit/9d9a0e81dbc9e73e6e3181f6bea9cabb69e49ea8))
|
|
||||||
|
|
||||||
# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)
|
# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
|
|
||||||
import com.mikepenz.aboutlibraries.plugin.DuplicateRule
|
|
||||||
import io.github.z4kn4fein.semver.toVersion
|
import io.github.z4kn4fein.semver.toVersion
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -12,7 +9,6 @@ plugins {
|
|||||||
alias(libs.plugins.compose.compiler)
|
alias(libs.plugins.compose.compiler)
|
||||||
alias(libs.plugins.devtools)
|
alias(libs.plugins.devtools)
|
||||||
alias(libs.plugins.about.libraries)
|
alias(libs.plugins.about.libraries)
|
||||||
alias(libs.plugins.about.libraries.android)
|
|
||||||
signing
|
signing
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +81,7 @@ dependencies {
|
|||||||
implementation(libs.koin.workmanager)
|
implementation(libs.koin.workmanager)
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
implementation(libs.about.libraries.core)
|
implementation(libs.about.libraries)
|
||||||
implementation(libs.about.libraries.m3)
|
|
||||||
|
|
||||||
// Ktor
|
// Ktor
|
||||||
implementation(libs.ktor.core)
|
implementation(libs.ktor.core)
|
||||||
@@ -131,7 +126,7 @@ buildscript {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.manager"
|
namespace = "app.revanced.manager"
|
||||||
compileSdk = 36
|
compileSdk = 35
|
||||||
buildToolsVersion = "35.0.1"
|
buildToolsVersion = "35.0.1"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -148,25 +143,13 @@ android {
|
|||||||
(preRelease?.substringAfterLast('.')?.toInt() ?: 99)
|
(preRelease?.substringAfterLast('.')?.toInt() ?: 99)
|
||||||
}
|
}
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
val resDir = file("src/main/res")
|
|
||||||
val locales = resDir.listFiles()
|
|
||||||
.orEmpty()
|
|
||||||
//noinspection WrongGradleMethod
|
|
||||||
.filter { it.isDirectory && it.name.matches(Regex("values-[a-z]{2}(-r[A-Z]{2})?")) }
|
|
||||||
//noinspection WrongGradleMethod
|
|
||||||
.map { it.name.removePrefix("values-").replace("-r", "-") }
|
|
||||||
.sorted()
|
|
||||||
//noinspection WrongGradleMethod
|
|
||||||
.joinToString(prefix = "{", separator = ",", postfix = "}") { "\"$it\"" }
|
|
||||||
|
|
||||||
buildConfigField("String[]", "SUPPORTED_LOCALES", locales)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
||||||
|
isPseudoLocalesEnabled = true
|
||||||
|
|
||||||
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||||
}
|
}
|
||||||
@@ -238,14 +221,20 @@ android {
|
|||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
aidl = true
|
aidl = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
|
||||||
androidResources {
|
android {
|
||||||
generateLocaleConfig = true
|
androidResources {
|
||||||
|
generateLocaleConfig = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
@@ -258,18 +247,6 @@ android {
|
|||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(17)
|
jvmToolchain(17)
|
||||||
compilerOptions {
|
|
||||||
jvmTarget = JvmTarget.JVM_17
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
aboutLibraries {
|
|
||||||
library {
|
|
||||||
// Enable the duplication mode, allows to merge, or link dependencies which relate
|
|
||||||
duplicationMode = DuplicateMode.MERGE
|
|
||||||
// Configure the duplication rule, to match "duplicates" with
|
|
||||||
duplicationRule = DuplicateRule.EXACT
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
version = 1.26.0-dev.20
|
version = 1.26.0-dev.16
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package app.revanced.manager
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
@@ -30,7 +30,7 @@ import app.revanced.manager.ui.model.navigation.ComplexParameter
|
|||||||
import app.revanced.manager.ui.model.navigation.Dashboard
|
import app.revanced.manager.ui.model.navigation.Dashboard
|
||||||
import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
|
import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
|
||||||
import app.revanced.manager.ui.model.navigation.Patcher
|
import app.revanced.manager.ui.model.navigation.Patcher
|
||||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||||
import app.revanced.manager.ui.model.navigation.Settings
|
import app.revanced.manager.ui.model.navigation.Settings
|
||||||
import app.revanced.manager.ui.model.navigation.Update
|
import app.revanced.manager.ui.model.navigation.Update
|
||||||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||||
@@ -41,7 +41,9 @@ import app.revanced.manager.ui.screen.PatchesSelectorScreen
|
|||||||
import app.revanced.manager.ui.screen.RequiredOptionsScreen
|
import app.revanced.manager.ui.screen.RequiredOptionsScreen
|
||||||
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
||||||
import app.revanced.manager.ui.screen.SettingsScreen
|
import app.revanced.manager.ui.screen.SettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.SourceSelectorScreen
|
||||||
import app.revanced.manager.ui.screen.UpdateScreen
|
import app.revanced.manager.ui.screen.UpdateScreen
|
||||||
|
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
||||||
import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
|
import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
|
||||||
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
|
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
|
||||||
import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen
|
import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen
|
||||||
@@ -59,10 +61,11 @@ import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
|||||||
import app.revanced.manager.util.EventEffect
|
import app.revanced.manager.util.EventEffect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import org.koin.androidx.compose.navigation.koinNavViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -94,23 +97,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
dynamicColor = dynamicColor,
|
dynamicColor = dynamicColor,
|
||||||
pureBlackTheme = pureBlackTheme
|
pureBlackTheme = pureBlackTheme
|
||||||
) {
|
) {
|
||||||
ReVancedManager(vm)
|
ReVancedManager()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReVancedManager(vm: MainViewModel) {
|
private fun ReVancedManager() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
EventEffect(vm.appSelectFlow) { app ->
|
|
||||||
navController.navigateComplex(
|
|
||||||
SelectedApplicationInfo,
|
|
||||||
SelectedApplicationInfo.ViewModelParams(app)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Dashboard,
|
startDestination = Dashboard,
|
||||||
@@ -141,7 +137,12 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
val data = it.toRoute<InstalledApplicationInfo>()
|
val data = it.toRoute<InstalledApplicationInfo>()
|
||||||
|
|
||||||
InstalledAppInfoScreen(
|
InstalledAppInfoScreen(
|
||||||
onPatchClick = vm::selectApp,
|
onPatchClick = { packageName ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedAppInfo,
|
||||||
|
SelectedAppInfo.ViewModelParams(packageName)
|
||||||
|
)
|
||||||
|
},
|
||||||
onBackClick = navController::popBackStack,
|
onBackClick = navController::popBackStack,
|
||||||
viewModel = koinViewModel { parametersOf(data.packageName) }
|
viewModel = koinViewModel { parametersOf(data.packageName) }
|
||||||
)
|
)
|
||||||
@@ -149,8 +150,20 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
|
|
||||||
composable<AppSelector> {
|
composable<AppSelector> {
|
||||||
AppSelectorScreen(
|
AppSelectorScreen(
|
||||||
onSelect = vm::selectApp,
|
onSelect = { packageName ->
|
||||||
onStorageSelect = vm::selectApp,
|
navController.navigateComplex(
|
||||||
|
SelectedAppInfo,
|
||||||
|
SelectedAppInfo.ViewModelParams(packageName)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onStorageSelect = { packageName, localPath ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedAppInfo,
|
||||||
|
SelectedAppInfo.ViewModelParams(
|
||||||
|
packageName, localPath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
onBackClick = navController::popBackStack
|
onBackClick = navController::popBackStack
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -178,13 +191,13 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
|
navigation<SelectedAppInfo>(startDestination = SelectedAppInfo.Main) {
|
||||||
composable<SelectedApplicationInfo.Main> {
|
composable<SelectedAppInfo.Main> {
|
||||||
val parentBackStackEntry = navController.navGraphEntry(it)
|
val parentBackStackEntry = navController.navGraphEntry(it)
|
||||||
val data =
|
val data =
|
||||||
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
parentBackStackEntry.getComplexArg<SelectedAppInfo.ViewModelParams>()
|
||||||
val viewModel =
|
val viewModel =
|
||||||
koinViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||||
parametersOf(data)
|
parametersOf(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,23 +211,47 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPatchSelectorClick = { app, patches, options ->
|
onPatchSelectorClick = { packageName, version, patchSelection, options ->
|
||||||
navController.navigateComplex(
|
navController.navigateComplex(
|
||||||
SelectedApplicationInfo.PatchesSelector,
|
SelectedAppInfo.PatchesSelector,
|
||||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
SelectedAppInfo.PatchesSelector.ViewModelParams(
|
||||||
app,
|
packageName,
|
||||||
patches,
|
version,
|
||||||
options
|
patchSelection,
|
||||||
|
options,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onRequiredOptions = { app, patches, options ->
|
onRequiredOptions = { packageName, version, patchSelection, options ->
|
||||||
navController.navigateComplex(
|
navController.navigateComplex(
|
||||||
SelectedApplicationInfo.RequiredOptions,
|
SelectedAppInfo.RequiredOptions,
|
||||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
SelectedAppInfo.PatchesSelector.ViewModelParams(
|
||||||
app,
|
packageName,
|
||||||
patches,
|
version,
|
||||||
options
|
patchSelection,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onVersionClick = { packageName, patchSelection, selectedVersion, local ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedAppInfo.VersionSelector,
|
||||||
|
SelectedAppInfo.VersionSelector.ViewModelParams(
|
||||||
|
packageName,
|
||||||
|
patchSelection,
|
||||||
|
selectedVersion,
|
||||||
|
local,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSourceClick = { packageName, version, selectedSource, local ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedAppInfo.SourceSelector,
|
||||||
|
SelectedAppInfo.SourceSelector.ViewModelParams(
|
||||||
|
packageName,
|
||||||
|
version,
|
||||||
|
selectedSource,
|
||||||
|
local,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -222,10 +259,10 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<SelectedApplicationInfo.PatchesSelector> {
|
composable<SelectedAppInfo.PatchesSelector> {
|
||||||
val data =
|
val data =
|
||||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
it.getComplexArg<SelectedAppInfo.PatchesSelector.ViewModelParams>()
|
||||||
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
|
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -239,10 +276,44 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<SelectedApplicationInfo.RequiredOptions> {
|
composable<SelectedAppInfo.VersionSelector> {
|
||||||
val data =
|
val data =
|
||||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
it.getComplexArg<SelectedAppInfo.VersionSelector.ViewModelParams>()
|
||||||
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
|
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||||
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
|
)
|
||||||
|
|
||||||
|
VersionSelectorScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onSave = { version ->
|
||||||
|
selectedAppInfoVm.updateVersion(version)
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
viewModel = koinViewModel { parametersOf(data) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<SelectedAppInfo.SourceSelector> {
|
||||||
|
val data =
|
||||||
|
it.getComplexArg<SelectedAppInfo.SourceSelector.ViewModelParams>()
|
||||||
|
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||||
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
|
)
|
||||||
|
|
||||||
|
SourceSelectorScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onSave = { source ->
|
||||||
|
selectedAppInfoVm.updateSource(source)
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
viewModel = koinViewModel { parametersOf(data) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<SelectedAppInfo.RequiredOptions> {
|
||||||
|
val data =
|
||||||
|
it.getComplexArg<SelectedAppInfo.PatchesSelector.ViewModelParams>()
|
||||||
|
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ class NetworkInfo(app: Application) {
|
|||||||
/**
|
/**
|
||||||
* Returns true if it is safe to download large files.
|
* Returns true if it is safe to download large files.
|
||||||
*/
|
*/
|
||||||
fun isSafe(ignoreMetered: Boolean) = isConnected() && (ignoreMetered || isUnmetered())
|
fun isSafe() = isConnected() && isUnmetered()
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ package app.revanced.manager.data.room.apps.downloaded
|
|||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -12,6 +11,9 @@ interface DownloadedAppDao {
|
|||||||
@Query("SELECT * FROM downloaded_app")
|
@Query("SELECT * FROM downloaded_app")
|
||||||
fun getAllApps(): Flow<List<DownloadedApp>>
|
fun getAllApps(): Flow<List<DownloadedApp>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName")
|
||||||
|
fun get(packageName: String): Flow<List<DownloadedApp>>
|
||||||
|
|
||||||
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version")
|
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version")
|
||||||
suspend fun get(packageName: String, version: String): DownloadedApp?
|
suspend fun get(packageName: String, version: String): DownloadedApp?
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package app.revanced.manager.di
|
package app.revanced.manager.di
|
||||||
|
|
||||||
import app.revanced.manager.ui.viewmodel.*
|
import app.revanced.manager.ui.viewmodel.*
|
||||||
import org.koin.core.module.dsl.*
|
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val viewModelModule = module {
|
val viewModelModule = module {
|
||||||
@@ -24,4 +24,6 @@ val viewModelModule = module {
|
|||||||
viewModelOf(::InstalledAppInfoViewModel)
|
viewModelOf(::InstalledAppInfoViewModel)
|
||||||
viewModelOf(::UpdatesSettingsViewModel)
|
viewModelOf(::UpdatesSettingsViewModel)
|
||||||
viewModelOf(::BundleListViewModel)
|
viewModelOf(::BundleListViewModel)
|
||||||
|
viewModelOf(::VersionSelectorViewModel)
|
||||||
|
viewModelOf(::SourceSelectorViewModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,4 @@ class PreferencesManager(
|
|||||||
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
||||||
|
|
||||||
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
|
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
|
||||||
|
|
||||||
val allowMeteredNetworks = booleanPreference("allow_metered_networks", false)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class DownloadedAppRepository(
|
|||||||
|
|
||||||
fun getAll() = dao.getAllApps().distinctUntilChanged()
|
fun getAll() = dao.getAllApps().distinctUntilChanged()
|
||||||
|
|
||||||
|
fun get(packageName: String) = dao.get(packageName)
|
||||||
|
|
||||||
fun getApkFileForApp(app: DownloadedApp): File =
|
fun getApkFileForApp(app: DownloadedApp): File =
|
||||||
getApkFileForDir(dir.resolve(app.directory))
|
getApkFileForDir(dir.resolve(app.directory))
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
|
|||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.patcher.patch.PatchBundle
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||||
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.simpleMessage
|
import app.revanced.manager.util.simpleMessage
|
||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
@@ -74,6 +75,17 @@ class PatchBundleRepository(
|
|||||||
|
|
||||||
val patchCountsFlow = bundleInfoFlow.map { it.mapValues { (_, info) -> info.patches.size } }
|
val patchCountsFlow = bundleInfoFlow.map { it.mapValues { (_, info) -> info.patches.size } }
|
||||||
|
|
||||||
|
fun suggestedVersions(packageName: String, patchSelection: PatchSelection) =
|
||||||
|
bundleInfoFlow.map {
|
||||||
|
val allPatches = patchSelection.flatMap { (uid, patches) ->
|
||||||
|
val bundle = it[uid] ?: return@flatMap emptyList()
|
||||||
|
bundle.patches.filter { patch -> patches.contains(patch.name) }
|
||||||
|
.map(PatchInfo::toPatcherPatch)
|
||||||
|
}.toSet()
|
||||||
|
|
||||||
|
allPatches.mostCommonCompatibleVersions(countUnusedPatches = true)[packageName]
|
||||||
|
}
|
||||||
|
|
||||||
val suggestedVersions = bundleInfoFlow.map {
|
val suggestedVersions = bundleInfoFlow.map {
|
||||||
val allPatches =
|
val allPatches =
|
||||||
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
||||||
@@ -286,29 +298,28 @@ class PatchBundleRepository(
|
|||||||
State(sources.toPersistentMap(), info.toPersistentMap())
|
State(sources.toPersistentMap(), info.toPersistentMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createLocal(createStream: suspend () -> InputStream) =
|
suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") {
|
||||||
dispatchAction("Add bundle") {
|
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
|
||||||
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
|
try {
|
||||||
try {
|
createStream().use { patches -> replace(patches) }
|
||||||
createStream().use { patches -> replace(patches) }
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
if (e is CancellationException) throw e
|
||||||
if (e is CancellationException) throw e
|
Log.e(tag, "Got exception while importing bundle", e)
|
||||||
Log.e(tag, "Got exception while importing bundle", e)
|
withContext(Dispatchers.Main) {
|
||||||
withContext(Dispatchers.Main) {
|
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
|
||||||
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteLocalFile()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
doReload()
|
deleteLocalFile()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doReload()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun createRemote(url: String, autoUpdate: Boolean) =
|
suspend fun createRemote(url: String, autoUpdate: Boolean) =
|
||||||
dispatchAction("Add bundle ($url)") { state ->
|
dispatchAction("Add bundle ($url)") { state ->
|
||||||
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle
|
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle
|
||||||
update(src, force = true)
|
update(src)
|
||||||
state.copy(sources = state.sources.put(src.uid, src))
|
state.copy(sources = state.sources.put(src.uid, src))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,38 +341,32 @@ class PatchBundleRepository(
|
|||||||
state.copy(sources = state.sources.put(uid, newSrc))
|
state.copy(sources = state.sources.put(uid, newSrc))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun update(
|
suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) {
|
||||||
vararg sources: RemotePatchBundle,
|
|
||||||
showToast: Boolean = false,
|
|
||||||
force: Boolean = false
|
|
||||||
) {
|
|
||||||
val uids = sources.map { it.uid }.toSet()
|
val uids = sources.map { it.uid }.toSet()
|
||||||
store.dispatch(Update(showToast = showToast, force = force) { it.uid in uids })
|
store.dispatch(Update(showToast = showToast) { it.uid in uids })
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true, redownload = true))
|
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates all bundles that should be automatically updated.
|
* Updates all bundles that should be automatically updated.
|
||||||
*/
|
*/
|
||||||
suspend fun updateCheck() =
|
suspend fun updateCheck() = store.dispatch(Update { it.autoUpdate })
|
||||||
store.dispatch(Update(force = prefs.allowMeteredNetworks.get()) { it.autoUpdate })
|
|
||||||
|
|
||||||
private inner class Update(
|
private inner class Update(
|
||||||
private val force: Boolean = false,
|
private val force: Boolean = false,
|
||||||
private val redownload: Boolean = false,
|
|
||||||
private val showToast: Boolean = false,
|
private val showToast: Boolean = false,
|
||||||
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
|
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
|
||||||
) : Action<State> {
|
) : Action<State> {
|
||||||
private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
|
private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
|
||||||
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) }
|
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) }
|
||||||
|
|
||||||
override fun toString() = if (redownload) "Redownload remote bundles" else "Update check"
|
override fun toString() = if (force) "Redownload remote bundles" else "Update check"
|
||||||
|
|
||||||
override suspend fun ActionContext.execute(
|
override suspend fun ActionContext.execute(
|
||||||
current: State
|
current: State
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
if (!networkInfo.isSafe(force)) {
|
if (!networkInfo.isSafe()) {
|
||||||
Log.d(tag, "Skipping update check because the network is down or metered.")
|
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||||
return@coroutineScope current
|
return@coroutineScope current
|
||||||
}
|
}
|
||||||
@@ -374,7 +379,7 @@ class PatchBundleRepository(
|
|||||||
Log.d(tag, "Updating patch bundle: ${it.name}")
|
Log.d(tag, "Updating patch bundle: ${it.name}")
|
||||||
|
|
||||||
val newVersion = with(it) {
|
val newVersion = with(it) {
|
||||||
if (redownload) downloadLatest() else update()
|
if (force) downloadLatest() else update()
|
||||||
} ?: return@async null
|
} ?: return@async null
|
||||||
|
|
||||||
it to newVersion
|
it to newVersion
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class PatchSelectionRepository(db: AppDatabase) {
|
|||||||
packageName = packageName
|
packageName = packageName
|
||||||
).also { dao.createSelection(it) }.uid
|
).also { dao.createSelection(it) }.uid
|
||||||
|
|
||||||
suspend fun getSelection(packageName: String): Map<Int, Set<String>> =
|
suspend fun getSelection(packageName: String): app.revanced.manager.util.PatchSelection =
|
||||||
dao.getSelectedPatches(packageName).mapValues { it.value.toSet() }
|
dao.getSelectedPatches(packageName).mapValues { it.value.toSet() }
|
||||||
|
|
||||||
suspend fun updateSelection(packageName: String, selection: Map<Int, Set<String>>) =
|
suspend fun updateSelection(packageName: String, selection: Map<Int, Set<String>>) =
|
||||||
|
|||||||
@@ -16,11 +16,8 @@ import io.ktor.http.isSuccess
|
|||||||
import io.ktor.utils.io.ByteReadChannel
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
import io.ktor.utils.io.core.isNotEmpty
|
import io.ktor.utils.io.core.isNotEmpty
|
||||||
import io.ktor.utils.io.core.readBytes
|
import io.ktor.utils.io.core.readBytes
|
||||||
import io.ktor.utils.io.exhausted
|
|
||||||
import io.ktor.utils.io.readRemaining
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.io.asSink
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
@@ -72,12 +69,14 @@ class HttpService(
|
|||||||
) {
|
) {
|
||||||
http.prepareGet(builder).execute { httpResponse ->
|
http.prepareGet(builder).execute { httpResponse ->
|
||||||
if (httpResponse.status.isSuccess()) {
|
if (httpResponse.status.isSuccess()) {
|
||||||
|
val channel: ByteReadChannel = httpResponse.body()
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val channel: ByteReadChannel = httpResponse.body()
|
while (!channel.isClosedForRead) {
|
||||||
val sink = outputStream.asSink()
|
|
||||||
while (!channel.exhausted()) {
|
|
||||||
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||||
packet.transferTo(sink)
|
while (packet.isNotEmpty) {
|
||||||
|
val bytes = packet.readBytes()
|
||||||
|
outputStream.write(bytes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ data class PatchInfo(
|
|||||||
if (pkg.packageName != packageName) return@any false
|
if (pkg.packageName != packageName) return@any false
|
||||||
if (pkg.versions == null) return@any true
|
if (pkg.versions == null) return@any true
|
||||||
|
|
||||||
versionName != null && versionName in pkg.versions
|
versionName == null || versionName in pkg.versions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import app.revanced.manager.patcher.toRemoteError
|
|||||||
import app.revanced.manager.plugin.downloader.GetScope
|
import app.revanced.manager.plugin.downloader.GetScope
|
||||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedSource
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
@@ -67,7 +67,9 @@ class PatcherWorker(
|
|||||||
private val rootInstaller: RootInstaller by inject()
|
private val rootInstaller: RootInstaller by inject()
|
||||||
|
|
||||||
class Args(
|
class Args(
|
||||||
val input: SelectedApp,
|
val packageName: String,
|
||||||
|
val version: String?,
|
||||||
|
val source: SelectedSource,
|
||||||
val output: String,
|
val output: String,
|
||||||
val selectedPatches: PatchSelection,
|
val selectedPatches: PatchSelection,
|
||||||
val options: Options,
|
val options: Options,
|
||||||
@@ -75,9 +77,7 @@ class PatcherWorker(
|
|||||||
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
||||||
val setInputFile: suspend (File) -> Unit,
|
val setInputFile: suspend (File) -> Unit,
|
||||||
val onEvent: (ProgressEvent) -> Unit,
|
val onEvent: (ProgressEvent) -> Unit,
|
||||||
) {
|
)
|
||||||
val packageName get() = input.packageName
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getForegroundInfo() =
|
override suspend fun getForegroundInfo() =
|
||||||
ForegroundInfo(
|
ForegroundInfo(
|
||||||
@@ -142,7 +142,7 @@ class PatcherWorker(
|
|||||||
val patchedApk = fs.tempDir.resolve("patched.apk")
|
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
if (args.input is SelectedApp.Installed) {
|
if (args.source is SelectedSource.Installed) {
|
||||||
installedAppRepository.get(args.packageName)?.let {
|
installedAppRepository.get(args.packageName)?.let {
|
||||||
if (it.installType == InstallType.MOUNT) {
|
if (it.installType == InstallType.MOUNT) {
|
||||||
rootInstaller.unmount(args.packageName)
|
rootInstaller.unmount(args.packageName)
|
||||||
@@ -155,32 +155,23 @@ class PatcherWorker(
|
|||||||
plugin,
|
plugin,
|
||||||
data,
|
data,
|
||||||
args.packageName,
|
args.packageName,
|
||||||
args.input.version,
|
args.version,
|
||||||
prefs.suggestedVersionSafeguard.get(),
|
prefs.suggestedVersionSafeguard.get(),
|
||||||
!prefs.disablePatchVersionCompatCheck.get(),
|
!prefs.disablePatchVersionCompatCheck.get(),
|
||||||
onDownload = { progress ->
|
) { progress ->
|
||||||
args.onEvent(
|
args.onEvent(
|
||||||
ProgressEvent.Progress(
|
ProgressEvent.Progress(
|
||||||
stepId = StepId.DownloadAPK,
|
stepId = StepId.DownloadAPK,
|
||||||
current = progress.first,
|
current = progress.first,
|
||||||
total = progress.second
|
total = progress.second
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
).also { args.setInputFile(it) }
|
}.also { args.setInputFile(it) }
|
||||||
|
|
||||||
val inputFile = when (val selectedApp = args.input) {
|
val inputFile = when (val source = args.source) {
|
||||||
is SelectedApp.Download -> {
|
is SelectedSource.Auto -> throw Exception("Auto source is not supported in worker.")
|
||||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
|
||||||
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(
|
|
||||||
selectedApp.data
|
|
||||||
)
|
|
||||||
|
|
||||||
download(plugin, data)
|
is SelectedSource.Plugin -> {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is SelectedApp.Search -> {
|
|
||||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||||
.firstNotNullOfOrNull { plugin ->
|
.firstNotNullOfOrNull { plugin ->
|
||||||
@@ -206,10 +197,10 @@ class PatcherWorker(
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
plugin.get(
|
plugin.get(
|
||||||
getScope,
|
getScope,
|
||||||
selectedApp.packageName,
|
args.packageName,
|
||||||
selectedApp.version
|
args.version
|
||||||
)
|
)
|
||||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
}?.takeIf { (_, version) -> args.version == null || version == args.version }
|
||||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||||
throw e
|
throw e
|
||||||
} catch (_: UserInteractionException) {
|
} catch (_: UserInteractionException) {
|
||||||
@@ -219,8 +210,10 @@ class PatcherWorker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
is SelectedSource.Downloaded -> File(source.path)
|
||||||
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir)
|
is SelectedSource.Local -> File(source.path)
|
||||||
|
|
||||||
|
is SelectedSource.Installed -> File(pm.getPackageInfo(args.packageName)!!.applicationInfo!!.sourceDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
val runtime = if (prefs.useProcessRuntime.get()) {
|
val runtime = if (prefs.useProcessRuntime.get()) {
|
||||||
@@ -258,9 +251,7 @@ class PatcherWorker(
|
|||||||
Result.failure()
|
Result.failure()
|
||||||
} finally {
|
} finally {
|
||||||
patchedApk.delete()
|
patchedApk.delete()
|
||||||
if (args.input is SelectedApp.Local && args.input.temporary) {
|
if (args.source is SelectedSource.Local) File(args.source.path).delete()
|
||||||
args.input.file.delete()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.eygraber.compose.placeholder.material3.placeholder
|
import io.github.fornewid.placeholder.material3.placeholder
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppIcon(
|
fun AppIcon(
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import com.eygraber.compose.placeholder.material3.placeholder
|
import io.github.fornewid.placeholder.material3.placeholder
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -47,6 +48,8 @@ fun AppLabel(
|
|||||||
shape = RoundedCornerShape(100)
|
shape = RoundedCornerShape(100)
|
||||||
)
|
)
|
||||||
.then(modifier),
|
.then(modifier),
|
||||||
style = style
|
style = style,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ fun LoadingIndicator(
|
|||||||
progress: () -> Float? = { null },
|
progress: () -> Float? = { null },
|
||||||
color: Color = ProgressIndicatorDefaults.circularColor,
|
color: Color = ProgressIndicatorDefaults.circularColor,
|
||||||
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
|
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
|
||||||
trackColor: Color = ProgressIndicatorDefaults.circularIndeterminateTrackColor,
|
trackColor: Color = ProgressIndicatorDefaults.circularTrackColor,
|
||||||
strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap
|
strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap
|
||||||
) {
|
) {
|
||||||
progress()?.let {
|
progress()?.let {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ fun Markdown(
|
|||||||
colors = markdownColor(
|
colors = markdownColor(
|
||||||
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
codeBackground = MaterialTheme.colorScheme.secondaryContainer,
|
codeBackground = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
codeText = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
linkText = MaterialTheme.colorScheme.primary
|
||||||
),
|
),
|
||||||
typography = markdownTypography(
|
typography = markdownTypography(
|
||||||
h1 = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),
|
h1 = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ fun SearchBar(
|
|||||||
) {
|
) {
|
||||||
val colors = SearchBarColors(
|
val colors = SearchBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
dividerColor = MaterialTheme.colorScheme.outline,
|
dividerColor = MaterialTheme.colorScheme.outline
|
||||||
inputFieldColors = SearchBarDefaults.inputFieldColors()
|
|
||||||
)
|
)
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ fun SearchView(
|
|||||||
) {
|
) {
|
||||||
val colors = SearchBarColors(
|
val colors = SearchBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
dividerColor = MaterialTheme.colorScheme.outline,
|
dividerColor = MaterialTheme.colorScheme.outline
|
||||||
inputFieldColors = SearchBarDefaults.inputFieldColors()
|
|
||||||
)
|
)
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import app.revanced.manager.R
|
|||||||
import app.revanced.manager.patcher.StepId
|
import app.revanced.manager.patcher.StepId
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
enum class StepCategory(@param:StringRes val displayName: Int) {
|
enum class StepCategory(@StringRes val displayName: Int) {
|
||||||
PREPARING(R.string.patcher_step_group_preparing),
|
PREPARING(R.string.patcher_step_group_preparing),
|
||||||
PATCHING(R.string.patcher_step_group_patching),
|
PATCHING(R.string.patcher_step_group_patching),
|
||||||
SAVING(R.string.patcher_step_group_saving)
|
SAVING(R.string.patcher_step_group_saving)
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package app.revanced.manager.ui.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
sealed interface SelectedApp : Parcelable {
|
|
||||||
val packageName: String
|
|
||||||
val version: String?
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Download(
|
|
||||||
override val packageName: String,
|
|
||||||
override val version: String?,
|
|
||||||
val data: ParceledDownloaderData
|
|
||||||
) : SelectedApp
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Search(override val packageName: String, override val version: String?) : SelectedApp
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Local(
|
|
||||||
override val packageName: String,
|
|
||||||
override val version: String,
|
|
||||||
val file: File,
|
|
||||||
val temporary: Boolean
|
|
||||||
) : SelectedApp
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Installed(
|
|
||||||
override val packageName: String,
|
|
||||||
override val version: String
|
|
||||||
) : SelectedApp
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package app.revanced.manager.ui.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
sealed class SelectedSource : Parcelable {
|
||||||
|
data object Auto : SelectedSource()
|
||||||
|
data object Installed : SelectedSource()
|
||||||
|
data class Downloaded(val path: String, val version: String) : SelectedSource()
|
||||||
|
data class Local(val path: String) : SelectedSource()
|
||||||
|
data class Plugin(val packageName: String?) : SelectedSource()
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package app.revanced.manager.ui.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
sealed class SelectedVersion : Parcelable {
|
||||||
|
data object Auto : SelectedVersion()
|
||||||
|
data object Any : SelectedVersion()
|
||||||
|
data class Specific(val version: String) : SelectedVersion()
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package app.revanced.manager.ui.model.navigation
|
package app.revanced.manager.ui.model.navigation
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedSource
|
||||||
|
import app.revanced.manager.ui.model.SelectedVersion
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@@ -23,10 +24,11 @@ data class InstalledApplicationInfo(val packageName: String)
|
|||||||
data class Update(val downloadOnScreenEntry: Boolean = false)
|
data class Update(val downloadOnScreenEntry: Boolean = false)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> {
|
data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams> {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ViewModelParams(
|
data class ViewModelParams(
|
||||||
val app: SelectedApp,
|
val packageName: String,
|
||||||
|
val localPath: String? = null,
|
||||||
val patches: PatchSelection? = null
|
val patches: PatchSelection? = null
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@@ -37,12 +39,35 @@ data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.V
|
|||||||
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
|
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ViewModelParams(
|
data class ViewModelParams(
|
||||||
val app: SelectedApp,
|
val packageName: String,
|
||||||
val currentSelection: PatchSelection?,
|
val version: String?,
|
||||||
|
val patchSelection: PatchSelection?,
|
||||||
val options: @RawValue Options,
|
val options: @RawValue Options,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object VersionSelector : ComplexParameter<VersionSelector.ViewModelParams> {
|
||||||
|
@Parcelize
|
||||||
|
data class ViewModelParams(
|
||||||
|
val packageName: String,
|
||||||
|
val patchSelection: PatchSelection,
|
||||||
|
val selectedVersion: SelectedVersion,
|
||||||
|
val localPath: String? = null,
|
||||||
|
) : Parcelable
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object SourceSelector : ComplexParameter<SourceSelector.ViewModelParams> {
|
||||||
|
@Parcelize
|
||||||
|
data class ViewModelParams(
|
||||||
|
val packageName: String,
|
||||||
|
val version: String?,
|
||||||
|
val selectedSource: SelectedSource,
|
||||||
|
val localPath: String? = null,
|
||||||
|
) : Parcelable
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams>
|
data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams>
|
||||||
}
|
}
|
||||||
@@ -51,7 +76,9 @@ data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.V
|
|||||||
data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
|
data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ViewModelParams(
|
data class ViewModelParams(
|
||||||
val selectedApp: SelectedApp,
|
val packageName: String,
|
||||||
|
val version: String?,
|
||||||
|
val selectedSource: SelectedSource,
|
||||||
val selectedPatches: PatchSelection,
|
val selectedPatches: PatchSelection,
|
||||||
val options: @RawValue Options
|
val options: @RawValue Options
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
|||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
import app.revanced.manager.ui.component.NonSuggestedVersionDialog
|
import app.revanced.manager.ui.component.NonSuggestedVersionDialog
|
||||||
import app.revanced.manager.ui.component.SearchView
|
import app.revanced.manager.ui.component.SearchView
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
|
||||||
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
|
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
|
||||||
import app.revanced.manager.util.APK_MIMETYPE
|
import app.revanced.manager.util.APK_MIMETYPE
|
||||||
import app.revanced.manager.util.EventEffect
|
import app.revanced.manager.util.EventEffect
|
||||||
@@ -54,13 +53,13 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppSelectorScreen(
|
fun AppSelectorScreen(
|
||||||
onSelect: (String) -> Unit,
|
onSelect: (packageName: String) -> Unit,
|
||||||
onStorageSelect: (SelectedApp.Local) -> Unit,
|
onStorageSelect: (packageName: String, path: String) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: AppSelectorViewModel = koinViewModel()
|
vm: AppSelectorViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
EventEffect(flow = vm.storageSelectionFlow) {
|
EventEffect(flow = vm.storageSelectionFlow) {
|
||||||
onStorageSelect(it)
|
onStorageSelect(it.first, it.second)
|
||||||
}
|
}
|
||||||
|
|
||||||
val pickApkLauncher =
|
val pickApkLauncher =
|
||||||
@@ -83,12 +82,12 @@ fun AppSelectorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.nonSuggestedVersionDialogSubject?.let {
|
// vm.nonSuggestedVersionDialogSubject?.let {
|
||||||
NonSuggestedVersionDialog(
|
// NonSuggestedVersionDialog(
|
||||||
suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
|
// suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
|
||||||
onDismiss = vm::dismissNonSuggestedVersionDialog
|
// onDismiss = vm::dismissNonSuggestedVersionDialog
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (search)
|
if (search)
|
||||||
SearchView(
|
SearchView(
|
||||||
@@ -115,8 +114,7 @@ fun AppSelectorScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
headlineContent = { AppLabel(app.packageInfo) },
|
headlineContent = { AppLabel(app.packageInfo) },
|
||||||
supportingContent = { Text(app.packageName) },
|
supportingContent = app.patches?.let {
|
||||||
trailingContent = app.patches?.let {
|
|
||||||
{
|
{
|
||||||
Text(
|
Text(
|
||||||
pluralStringResource(
|
pluralStringResource(
|
||||||
@@ -214,12 +212,7 @@ fun AppSelectorScreen(
|
|||||||
defaultText = app.packageName
|
defaultText = app.packageName
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = app.patches?.let {
|
||||||
suggestedVersions[app.packageName]?.let {
|
|
||||||
Text(stringResource(R.string.suggested_version_info, it))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trailingContent = app.patches?.let {
|
|
||||||
{
|
{
|
||||||
Text(
|
Text(
|
||||||
pluralStringResource(
|
pluralStringResource(
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SecondaryTabRow
|
import androidx.compose.material3.TabRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
@@ -53,7 +53,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalResources
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -101,7 +100,6 @@ fun DashboardScreen(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
val androidContext = LocalContext.current
|
val androidContext = LocalContext.current
|
||||||
val resources = LocalResources.current
|
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState(
|
val pagerState = rememberPagerState(
|
||||||
initialPage = DashboardPage.DASHBOARD.ordinal,
|
initialPage = DashboardPage.DASHBOARD.ordinal,
|
||||||
@@ -236,7 +234,7 @@ fun DashboardScreen(
|
|||||||
when (pagerState.currentPage) {
|
when (pagerState.currentPage) {
|
||||||
DashboardPage.DASHBOARD.ordinal -> {
|
DashboardPage.DASHBOARD.ordinal -> {
|
||||||
if (availablePatches < 1) {
|
if (availablePatches < 1) {
|
||||||
androidContext.toast(resources.getString(R.string.no_patch_found))
|
androidContext.toast(androidContext.getString(R.string.no_patch_found))
|
||||||
composableScope.launch {
|
composableScope.launch {
|
||||||
pagerState.animateScrollToPage(
|
pagerState.animateScrollToPage(
|
||||||
DashboardPage.BUNDLES.ordinal
|
DashboardPage.BUNDLES.ordinal
|
||||||
@@ -261,7 +259,7 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(Modifier.padding(paddingValues)) {
|
Column(Modifier.padding(paddingValues)) {
|
||||||
SecondaryTabRow(
|
TabRow(
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalResources
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
@@ -70,7 +69,6 @@ fun PatcherScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val resources = LocalResources.current
|
|
||||||
val exportApkLauncher =
|
val exportApkLauncher =
|
||||||
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export)
|
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export)
|
||||||
|
|
||||||
@@ -81,7 +79,7 @@ fun PatcherScreen(
|
|||||||
|
|
||||||
fun onPageBack() = when {
|
fun onPageBack() = when {
|
||||||
patcherSucceeded == null -> showDismissConfirmationDialog = true
|
patcherSucceeded == null -> showDismissConfirmationDialog = true
|
||||||
viewModel.isInstalling -> context.toast(resources.getString(R.string.patcher_install_in_progress))
|
viewModel.isInstalling -> context.toast(context.getString(R.string.patcher_install_in_progress))
|
||||||
else -> onLeave()
|
else -> onLeave()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import androidx.compose.material3.ListItem
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SecondaryScrollableTabRow
|
import androidx.compose.material3.ScrollableTabRow
|
||||||
import androidx.compose.material3.SmallFloatingActionButton
|
import androidx.compose.material3.SmallFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -49,12 +49,10 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
@@ -83,11 +81,9 @@ import app.revanced.manager.util.Options
|
|||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.isScrollingUp
|
import app.revanced.manager.util.isScrollingUp
|
||||||
import app.revanced.manager.util.transparentListItemColors
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.flow.sample
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, FlowPreview::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PatchesSelectorScreen(
|
fun PatchesSelectorScreen(
|
||||||
onSave: (PatchSelection?, Options) -> Unit,
|
onSave: (PatchSelection?, Options) -> Unit,
|
||||||
@@ -235,8 +231,7 @@ fun PatchesSelectorScreen(
|
|||||||
viewModel.selectionWarningEnabled -> showSelectionWarning = true
|
viewModel.selectionWarningEnabled -> showSelectionWarning = true
|
||||||
|
|
||||||
// Show universal warning if universal patch is selected and the toggle is off
|
// Show universal warning if universal patch is selected and the toggle is off
|
||||||
patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning =
|
patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning = true
|
||||||
true
|
|
||||||
|
|
||||||
// Toggle the patch otherwise
|
// Toggle the patch otherwise
|
||||||
else -> viewModel.togglePatch(uid, patch)
|
else -> viewModel.togglePatch(uid, patch)
|
||||||
@@ -365,21 +360,6 @@ fun PatchesSelectorScreen(
|
|||||||
) {
|
) {
|
||||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||||
}
|
}
|
||||||
|
|
||||||
val isScrollingUp =
|
|
||||||
patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp()
|
|
||||||
val expanded by produceState(true, isScrollingUp) {
|
|
||||||
val state = isScrollingUp ?: return@produceState
|
|
||||||
value = state.value
|
|
||||||
|
|
||||||
// Use snapshotFlow and sample to prevent the value from changing too often.
|
|
||||||
snapshotFlow { state.value }
|
|
||||||
.sample(333L)
|
|
||||||
.collect {
|
|
||||||
value = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HapticExtendedFloatingActionButton(
|
HapticExtendedFloatingActionButton(
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
@@ -395,7 +375,8 @@ fun PatchesSelectorScreen(
|
|||||||
contentDescription = stringResource(R.string.save)
|
contentDescription = stringResource(R.string.save)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
expanded = expanded,
|
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
|
||||||
|
?: true,
|
||||||
onClick = {
|
onClick = {
|
||||||
onSave(viewModel.getCustomSelection(), viewModel.getOptions())
|
onSave(viewModel.getCustomSelection(), viewModel.getOptions())
|
||||||
}
|
}
|
||||||
@@ -411,7 +392,7 @@ fun PatchesSelectorScreen(
|
|||||||
.padding(top = 16.dp)
|
.padding(top = 16.dp)
|
||||||
) {
|
) {
|
||||||
if (bundles.size > 1) {
|
if (bundles.size > 1) {
|
||||||
SecondaryScrollableTabRow(
|
ScrollableTabRow(
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
) {
|
) {
|
||||||
@@ -517,7 +498,7 @@ private fun PatchItem(
|
|||||||
leadingContent = {
|
leadingContent = {
|
||||||
HapticCheckbox(
|
HapticCheckbox(
|
||||||
checked = selected,
|
checked = selected,
|
||||||
onCheckedChange = { onToggle() },
|
onCheckedChange = null,
|
||||||
enabled = compatible
|
enabled = compatible
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SecondaryScrollableTabRow
|
import androidx.compose.material3.ScrollableTabRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
@@ -106,7 +106,7 @@ fun RequiredOptionsScreen(
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
if (list.isEmpty()) return@Column
|
if (list.isEmpty()) return@Column
|
||||||
else if (list.size > 1) SecondaryScrollableTabRow(
|
else if (list.size > 1) ScrollableTabRow(
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||||
@@ -21,82 +17,69 @@ import androidx.compose.material3.ListItem
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalResources
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.platform.NetworkInfo
|
import app.revanced.manager.data.platform.NetworkInfo
|
||||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
|
||||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
|
||||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
|
||||||
import app.revanced.manager.ui.component.AppInfo
|
import app.revanced.manager.ui.component.AppInfo
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
|
||||||
import app.revanced.manager.ui.component.NotificationCard
|
import app.revanced.manager.ui.component.NotificationCard
|
||||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedSource
|
||||||
|
import app.revanced.manager.ui.model.SelectedVersion
|
||||||
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||||
import app.revanced.manager.util.EventEffect
|
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.enabled
|
import app.revanced.manager.util.enabled
|
||||||
|
import app.revanced.manager.util.patchCount
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.transparentListItemColors
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectedAppInfoScreen(
|
fun SelectedAppInfoScreen(
|
||||||
onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit,
|
onPatchSelectorClick: (packageName: String, version: String?, PatchSelection?, Options) -> Unit,
|
||||||
onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit,
|
onRequiredOptions: (packageName: String, version: String?, PatchSelection?, Options) -> Unit,
|
||||||
onPatchClick: () -> Unit,
|
onPatchClick: () -> Unit,
|
||||||
|
onVersionClick: (packageName: String, patchSelection: PatchSelection, selectedVersion: SelectedVersion, localPath: String?) -> Unit,
|
||||||
|
onSourceClick: (packageName: String, version: String?, SelectedSource, localPath: String?) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: SelectedAppInfoViewModel
|
vm: SelectedAppInfoViewModel
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val resources = LocalResources.current
|
|
||||||
val networkInfo = koinInject<NetworkInfo>()
|
val networkInfo = koinInject<NetworkInfo>()
|
||||||
val networkConnected = remember { networkInfo.isConnected() }
|
val networkConnected = remember { networkInfo.isConnected() }
|
||||||
val networkMetered = remember { !networkInfo.isUnmetered() }
|
val networkMetered = remember { !networkInfo.isUnmetered() }
|
||||||
|
|
||||||
val packageName = vm.selectedApp.packageName
|
val packageName = vm.packageName
|
||||||
val version = vm.selectedApp.version
|
|
||||||
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
|
|
||||||
|
|
||||||
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
|
||||||
val patches by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val selectedPatchCount = patches.values.sumOf { it.size }
|
|
||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.StartActivityForResult(),
|
|
||||||
onResult = vm::handlePluginActivityResult
|
|
||||||
)
|
|
||||||
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
|
||||||
launcher.launch(intent)
|
|
||||||
}
|
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
|
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
|
val selectedVersion by vm.selectedVersion.collectAsStateWithLifecycle()
|
||||||
|
val resolvedVersion by vm.resolvedVersion.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
|
val selectedSource by vm.selectedSource.collectAsStateWithLifecycle()
|
||||||
|
val resolvedSource by vm.resolvedSource.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
|
val customSelection by vm.customSelection.collectAsStateWithLifecycle(null)
|
||||||
|
val fullPatchSelection by vm.patchSelection.collectAsStateWithLifecycle(emptyMap())
|
||||||
|
val patchCount = fullPatchSelection.patchCount
|
||||||
|
|
||||||
|
val incompatibleCount by vm.incompatiblePatchCount.collectAsStateWithLifecycle(0)
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -119,18 +102,18 @@ fun SelectedAppInfoScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = patchClick@{
|
onClick = patchClick@{
|
||||||
if (selectedPatchCount == 0) {
|
if (patchCount == 0) {
|
||||||
context.toast(resources.getString(R.string.no_patches_selected))
|
context.toast(context.getString(R.string.no_patches_selected))
|
||||||
|
|
||||||
return@patchClick
|
return@patchClick
|
||||||
}
|
}
|
||||||
|
|
||||||
composableScope.launch {
|
composableScope.launch {
|
||||||
if (!vm.hasSetRequiredOptions(patches)) {
|
if (!vm.hasSetRequiredOptions(fullPatchSelection)) {
|
||||||
onRequiredOptions(
|
onRequiredOptions(
|
||||||
vm.selectedApp,
|
vm.packageName,
|
||||||
vm.getCustomPatches(bundles, allowIncompatiblePatches),
|
resolvedVersion,
|
||||||
vm.options
|
customSelection,
|
||||||
|
vm.options,
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@@ -142,94 +125,96 @@ fun SelectedAppInfoScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
|
|
||||||
|
|
||||||
if (vm.showSourceSelector) {
|
|
||||||
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
|
|
||||||
|
|
||||||
AppSourceSelectorDialog(
|
|
||||||
plugins = plugins,
|
|
||||||
installedApp = vm.installedAppData,
|
|
||||||
searchApp = SelectedApp.Search(
|
|
||||||
vm.packageName,
|
|
||||||
vm.desiredVersion
|
|
||||||
),
|
|
||||||
activeSearchJob = vm.activePluginAction,
|
|
||||||
hasRoot = vm.hasRoot,
|
|
||||||
onDismissRequest = vm::dismissSourceSelector,
|
|
||||||
onSelectPlugin = vm::searchUsingPlugin,
|
|
||||||
requiredVersion = requiredVersion,
|
|
||||||
onSelect = {
|
|
||||||
vm.selectedApp = it
|
|
||||||
vm.dismissSourceSelector()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnWithScrollbar(
|
ColumnWithScrollbar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
|
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
|
||||||
Text(
|
vm.selectedAppInfo?.let {
|
||||||
version ?: stringResource(R.string.selected_app_meta_any_version),
|
Text(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
it.packageName,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PageItem(
|
PageItem(
|
||||||
R.string.patch_selector_item,
|
R.string.patch_selector_item,
|
||||||
stringResource(
|
stringResource(R.string.patch_selector_item_description, patchCount),
|
||||||
R.string.patch_selector_item_description,
|
|
||||||
selectedPatchCount
|
|
||||||
),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onPatchSelectorClick(
|
onPatchSelectorClick(
|
||||||
vm.selectedApp,
|
vm.packageName,
|
||||||
vm.getCustomPatches(
|
resolvedVersion,
|
||||||
bundles,
|
customSelection,
|
||||||
allowIncompatiblePatches
|
|
||||||
),
|
|
||||||
vm.options
|
vm.options
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
extraDescription = if (incompatibleCount > 0) { {
|
||||||
|
Text(
|
||||||
|
"$incompatibleCount incompatible",
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
} } else null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val versionText = resolvedVersion ?: "Any available version"
|
||||||
|
val versionDescription = if (selectedVersion is SelectedVersion.Auto)
|
||||||
|
"Auto ($versionText)" // stringResource(R.string.selected_app_meta_auto_version, actualVersion)
|
||||||
|
else versionText
|
||||||
|
|
||||||
|
PageItem(
|
||||||
|
R.string.version_selector_item,
|
||||||
|
versionDescription,
|
||||||
|
onClick = {
|
||||||
|
onVersionClick(
|
||||||
|
packageName,
|
||||||
|
fullPatchSelection,
|
||||||
|
selectedVersion,
|
||||||
|
vm.localPath,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
val sourceText = when (val source = resolvedSource) {
|
||||||
|
is SelectedSource.Installed -> "Installed APK"
|
||||||
|
is SelectedSource.Downloaded -> "Downloaded APK"
|
||||||
|
is SelectedSource.Local -> "Local APK"
|
||||||
|
is SelectedSource.Plugin -> {
|
||||||
|
source.packageName ?: "Any available downloader"
|
||||||
|
}
|
||||||
|
else -> "Auto"
|
||||||
|
}
|
||||||
|
val sourceDescription = if (selectedSource is SelectedSource.Auto)
|
||||||
|
"Auto ($sourceText)" // stringResource(R.string.selected_app_meta_auto_version, actualVersion)
|
||||||
|
else sourceText
|
||||||
|
|
||||||
PageItem(
|
PageItem(
|
||||||
R.string.apk_source_selector_item,
|
R.string.apk_source_selector_item,
|
||||||
when (val app = vm.selectedApp) {
|
sourceDescription,
|
||||||
is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
|
onClick = { onSourceClick(
|
||||||
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
|
packageName,
|
||||||
is SelectedApp.Download -> stringResource(
|
resolvedVersion,
|
||||||
R.string.apk_source_downloader,
|
selectedSource,
|
||||||
plugins.find { it.packageName == app.data.pluginPackageName }?.name
|
vm.localPath,
|
||||||
?: app.data.pluginPackageName
|
) },
|
||||||
)
|
|
||||||
|
|
||||||
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
vm.showSourceSelector()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
error?.let {
|
error?.let {
|
||||||
Text(
|
Text(
|
||||||
stringResource(it.resourceId),
|
stringResource(it.resourceId),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
modifier = Modifier.padding(horizontal = 24.dp)
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
if (resolvedSource is SelectedSource.Plugin) Column(
|
||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
val needsInternet =
|
|
||||||
vm.selectedApp.let { it is SelectedApp.Search || it is SelectedApp.Download }
|
|
||||||
|
|
||||||
when {
|
when {
|
||||||
!needsInternet -> {}
|
|
||||||
!networkConnected -> {
|
!networkConnected -> {
|
||||||
NotificationCard(
|
NotificationCard(
|
||||||
isWarning = true,
|
isWarning = true,
|
||||||
@@ -238,7 +223,6 @@ fun SelectedAppInfoScreen(
|
|||||||
onDismiss = null
|
onDismiss = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
networkMetered -> {
|
networkMetered -> {
|
||||||
NotificationCard(
|
NotificationCard(
|
||||||
isWarning = true,
|
isWarning = true,
|
||||||
@@ -254,11 +238,17 @@ fun SelectedAppInfoScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) {
|
private fun PageItem(
|
||||||
|
@StringRes title: Int,
|
||||||
|
description: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
extraDescription: @Composable (ColumnScope.() -> Unit)? = null,
|
||||||
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(onClick = onClick)
|
.clickable(enabled, onClick = onClick)
|
||||||
.padding(start = 8.dp),
|
.enabled(enabled),
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(title),
|
stringResource(title),
|
||||||
@@ -267,99 +257,17 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () ->
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(
|
Column {
|
||||||
description,
|
Text(
|
||||||
color = MaterialTheme.colorScheme.outline,
|
description,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
color = MaterialTheme.colorScheme.outline,
|
||||||
)
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
extraDescription?.invoke(this)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Icon(Icons.AutoMirrored.Outlined.ArrowRight, null)
|
Icon(Icons.AutoMirrored.Outlined.ArrowRight, null)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun AppSourceSelectorDialog(
|
|
||||||
plugins: List<LoadedDownloaderPlugin>,
|
|
||||||
installedApp: Pair<SelectedApp.Installed, InstalledApp?>?,
|
|
||||||
searchApp: SelectedApp.Search,
|
|
||||||
activeSearchJob: String?,
|
|
||||||
hasRoot: Boolean,
|
|
||||||
requiredVersion: String?,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
onSelectPlugin: (LoadedDownloaderPlugin) -> Unit,
|
|
||||||
onSelect: (SelectedApp) -> Unit,
|
|
||||||
) {
|
|
||||||
val canSelect = activeSearchJob == null
|
|
||||||
|
|
||||||
AlertDialogExtended(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onDismissRequest) {
|
|
||||||
Text(stringResource(R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = { Text(stringResource(R.string.app_source_dialog_title)) },
|
|
||||||
textHorizontalPadding = PaddingValues(horizontal = 0.dp),
|
|
||||||
text = {
|
|
||||||
LazyColumn {
|
|
||||||
item(key = "auto") {
|
|
||||||
val hasPlugins = plugins.isNotEmpty()
|
|
||||||
ListItem(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) }
|
|
||||||
.enabled(hasPlugins),
|
|
||||||
headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) },
|
|
||||||
supportingContent = {
|
|
||||||
Text(
|
|
||||||
if (hasPlugins)
|
|
||||||
stringResource(R.string.app_source_dialog_option_auto_description)
|
|
||||||
else
|
|
||||||
stringResource(R.string.app_source_dialog_option_auto_unavailable)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
colors = transparentListItemColors
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
installedApp?.let { (app, meta) ->
|
|
||||||
item(key = "installed") {
|
|
||||||
val (usable, text) = when {
|
|
||||||
// Mounted apps must be unpatched before patching, which cannot be done without root access.
|
|
||||||
meta?.installType == InstallType.MOUNT && !hasRoot -> false to stringResource(
|
|
||||||
R.string.app_source_dialog_option_installed_no_root
|
|
||||||
)
|
|
||||||
// Patching already patched apps is not allowed because patches expect unpatched apps.
|
|
||||||
meta?.installType == InstallType.DEFAULT -> false to stringResource(R.string.already_patched)
|
|
||||||
// Version does not match suggested version.
|
|
||||||
requiredVersion != null && app.version != requiredVersion -> false to stringResource(
|
|
||||||
R.string.app_source_dialog_option_installed_version_not_suggested,
|
|
||||||
app.version
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> true to app.version
|
|
||||||
}
|
|
||||||
ListItem(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(enabled = canSelect && usable) { onSelect(app) }
|
|
||||||
.enabled(usable),
|
|
||||||
headlineContent = { Text(stringResource(R.string.installed)) },
|
|
||||||
supportingContent = { Text(text) },
|
|
||||||
colors = transparentListItemColors
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items(plugins, key = { "plugin_${it.packageName}" }) { plugin ->
|
|
||||||
ListItem(
|
|
||||||
modifier = Modifier.clickable(enabled = canSelect) { onSelectPlugin(plugin) },
|
|
||||||
headlineContent = { Text(plugin.name) },
|
|
||||||
trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == plugin.packageName },
|
|
||||||
colors = transparentListItemColors
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -22,8 +22,8 @@ import app.revanced.manager.ui.model.navigation.Settings
|
|||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
private data class Section(
|
private data class Section(
|
||||||
@param:StringRes val name: Int,
|
@StringRes val name: Int,
|
||||||
@param:StringRes val description: Int,
|
@StringRes val description: Int,
|
||||||
val image: ImageVector,
|
val image: ImageVector,
|
||||||
val destination: Settings.Destination,
|
val destination: Settings.Destination,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
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.haptics.HapticExtendedFloatingActionButton
|
||||||
|
import app.revanced.manager.ui.model.SelectedSource
|
||||||
|
import app.revanced.manager.ui.viewmodel.SourceSelectorViewModel
|
||||||
|
import app.revanced.manager.util.enabled
|
||||||
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SourceSelectorScreen(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onSave: (source: SelectedSource) -> Unit,
|
||||||
|
viewModel: SourceSelectorViewModel,
|
||||||
|
) {
|
||||||
|
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
||||||
|
val plugins by viewModel.plugins.collectAsStateWithLifecycle(emptyList())
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
AppTopBar(
|
||||||
|
title = { Text("Select source") },
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
HapticExtendedFloatingActionButton(
|
||||||
|
text = { Text(stringResource(R.string.save)) },
|
||||||
|
icon = { Icon(Icons.Outlined.Save, null) },
|
||||||
|
onClick = { onSave(viewModel.selectedSource) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
LazyColumnWithScrollbar (
|
||||||
|
contentPadding = paddingValues,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SourceOption(
|
||||||
|
isSelected = viewModel.selectedSource == SelectedSource.Auto,
|
||||||
|
onSelect = { viewModel.selectSource(SelectedSource.Auto) },
|
||||||
|
headlineContent = { Text("Auto (Recommended)") },
|
||||||
|
supportingContent = { Text("Automatically select the best available source") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
SourceOption(
|
||||||
|
isSelected = viewModel.selectedSource == SelectedSource.Plugin(null),
|
||||||
|
onSelect = { viewModel.selectSource(SelectedSource.Plugin(null)) },
|
||||||
|
headlineContent = { Text("Any available downloader") },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.localApp?.let { option ->
|
||||||
|
item {
|
||||||
|
HorizontalDivider()
|
||||||
|
SourceOption(
|
||||||
|
sourceOption = option,
|
||||||
|
isSelected = viewModel.selectedSource == option.source,
|
||||||
|
onSelect = viewModel::selectSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.installedSource?.let { option ->
|
||||||
|
item {
|
||||||
|
HorizontalDivider()
|
||||||
|
SourceOption(
|
||||||
|
sourceOption = option,
|
||||||
|
isSelected = viewModel.selectedSource == option.source,
|
||||||
|
onSelect = viewModel::selectSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadedApps.isNotEmpty()) item { HorizontalDivider() }
|
||||||
|
items(downloadedApps, key = { it.key }) { option ->
|
||||||
|
SourceOption(
|
||||||
|
sourceOption = option,
|
||||||
|
isSelected = viewModel.selectedSource == option.source,
|
||||||
|
onSelect = viewModel::selectSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugins.isNotEmpty()) item { HorizontalDivider() }
|
||||||
|
items(plugins, key = { it.key }) { option ->
|
||||||
|
SourceOption(
|
||||||
|
sourceOption = option,
|
||||||
|
isSelected = viewModel.selectedSource == option.source,
|
||||||
|
onSelect = viewModel::selectSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SourceOption(
|
||||||
|
sourceOption: SourceSelectorViewModel.SourceOption,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onSelect: (SelectedSource) -> Unit,
|
||||||
|
) = SourceOption(
|
||||||
|
isSelected = isSelected,
|
||||||
|
onSelect = { onSelect(sourceOption.source) },
|
||||||
|
overlineContent = sourceOption.category?.let {{ Text(it) }},
|
||||||
|
headlineContent = { Text(sourceOption.title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||||
|
supportingContent = sourceOption.disableReason?.let {{ Text(it.message) }},
|
||||||
|
enabled = sourceOption.disableReason == null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SourceOption(
|
||||||
|
isSelected: Boolean,
|
||||||
|
onSelect: () -> Unit,
|
||||||
|
headlineContent: @Composable (() -> Unit),
|
||||||
|
supportingContent: @Composable (() -> Unit)? = null,
|
||||||
|
overlineContent: @Composable (() -> Unit)? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(enabled) { onSelect() }
|
||||||
|
.enabled(enabled),
|
||||||
|
leadingContent = {
|
||||||
|
RadioButton(
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = headlineContent,
|
||||||
|
supportingContent = supportingContent,
|
||||||
|
overlineContent = overlineContent,
|
||||||
|
colors = transparentListItemColors
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
|
import app.revanced.manager.ui.model.SelectedVersion
|
||||||
|
import app.revanced.manager.ui.viewmodel.VersionSelectorViewModel
|
||||||
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun VersionSelectorScreen(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onSave: (version: SelectedVersion) -> Unit,
|
||||||
|
viewModel: VersionSelectorViewModel,
|
||||||
|
) {
|
||||||
|
val versions by viewModel.availableVersions.collectAsStateWithLifecycle(emptyList())
|
||||||
|
val downloadedVersions by viewModel.downloadedVersions.collectAsStateWithLifecycle(emptyList())
|
||||||
|
val localVersion by viewModel.localVersion.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
AppTopBar(
|
||||||
|
title = { Text("Select version") },
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
actions = {
|
||||||
|
IconButton({}) {
|
||||||
|
Icon(Icons.Outlined.MoreVert, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
HapticExtendedFloatingActionButton(
|
||||||
|
text = { Text(stringResource(R.string.save)) },
|
||||||
|
icon = { Icon(Icons.Outlined.Save, contentDescription = null) },
|
||||||
|
onClick = { onSave(viewModel.selectedVersion) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
VersionOption(
|
||||||
|
version = SelectedVersion.Auto,
|
||||||
|
isSelected = viewModel.selectedVersion is SelectedVersion.Auto,
|
||||||
|
onSelect = viewModel::selectVersion,
|
||||||
|
headlineContent = { Text("Auto (Recommended)") },
|
||||||
|
supportingContent = { Text("Automatically select the best available version") }
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
if (versions.isNotEmpty()) {
|
||||||
|
LazyColumn {
|
||||||
|
items(versions, key = { it.first.version }) { version ->
|
||||||
|
val isDownloaded = downloadedVersions.contains(version.first.version)
|
||||||
|
val isInstalled = viewModel.installedAppVersion == version.first.version
|
||||||
|
val isLocal = localVersion == version.first.version
|
||||||
|
|
||||||
|
val overlineText = when {
|
||||||
|
isLocal -> "Local"
|
||||||
|
isDownloaded && isInstalled -> "Downloaded, Installed"
|
||||||
|
isDownloaded -> "Downloaded"
|
||||||
|
isInstalled -> "Installed"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
VersionOption(
|
||||||
|
version = version.first,
|
||||||
|
isSelected = viewModel.selectedVersion == version.first,
|
||||||
|
onSelect = viewModel::selectVersion,
|
||||||
|
headlineContent = { Text(version.first.version) },
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
"${version.second.let { if (it == 0) "No" else it }} incompatible patches"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
overlineContent = overlineText?.let { { Text(it) } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VersionOption(
|
||||||
|
version = SelectedVersion.Any,
|
||||||
|
isSelected = viewModel.selectedVersion is SelectedVersion.Any,
|
||||||
|
onSelect = viewModel::selectVersion,
|
||||||
|
headlineContent = { Text("Any available version") },
|
||||||
|
supportingContent = { Text("Use any available version regardless of compatibility") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VersionOption(
|
||||||
|
version: SelectedVersion,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onSelect: (SelectedVersion) -> Unit,
|
||||||
|
headlineContent: @Composable (() -> Unit),
|
||||||
|
supportingContent: @Composable (() -> Unit)? = null,
|
||||||
|
overlineContent: @Composable (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { onSelect(version) },
|
||||||
|
leadingContent = {
|
||||||
|
RadioButton(
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = headlineContent,
|
||||||
|
supportingContent = supportingContent,
|
||||||
|
trailingContent = overlineContent,
|
||||||
|
colors = transparentListItemColors
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.revanced.manager.ui.screen.settings
|
package app.revanced.manager.ui.screen.settings
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@@ -41,7 +40,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalResources
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.hideFromAccessibility
|
import androidx.compose.ui.semantics.hideFromAccessibility
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
@@ -69,9 +67,8 @@ fun AboutSettingsScreen(
|
|||||||
viewModel: AboutViewModel = koinViewModel()
|
viewModel: AboutViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val resources = LocalResources.current
|
|
||||||
// painterResource() is broken on release builds for some reason.
|
// painterResource() is broken on release builds for some reason.
|
||||||
val icon = rememberDrawablePainter(drawable = remember(resources) {
|
val icon = rememberDrawablePainter(drawable = remember {
|
||||||
AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring)
|
AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -79,7 +76,7 @@ fun AboutSettingsScreen(
|
|||||||
viewModel.socials.partition(ReVancedSocial::preferred)
|
viewModel.socials.partition(ReVancedSocial::preferred)
|
||||||
}
|
}
|
||||||
|
|
||||||
val preferredSocialButtons = remember(resources, preferredSocials, viewModel.donate, viewModel.contact) {
|
val preferredSocialButtons = remember(preferredSocials, viewModel.donate, viewModel.contact) {
|
||||||
preferredSocials.map {
|
preferredSocials.map {
|
||||||
Triple(
|
Triple(
|
||||||
getSocialIcon(it.name),
|
getSocialIcon(it.name),
|
||||||
@@ -92,7 +89,7 @@ fun AboutSettingsScreen(
|
|||||||
viewModel.donate?.let {
|
viewModel.donate?.let {
|
||||||
Triple(
|
Triple(
|
||||||
Icons.Outlined.FavoriteBorder,
|
Icons.Outlined.FavoriteBorder,
|
||||||
resources.getString(R.string.donate),
|
context.getString(R.string.donate),
|
||||||
third = {
|
third = {
|
||||||
context.openUrl(it)
|
context.openUrl(it)
|
||||||
}
|
}
|
||||||
@@ -101,7 +98,7 @@ fun AboutSettingsScreen(
|
|||||||
viewModel.contact?.let {
|
viewModel.contact?.let {
|
||||||
Triple(
|
Triple(
|
||||||
Icons.Outlined.MailOutline,
|
Icons.Outlined.MailOutline,
|
||||||
resources.getString(R.string.contact),
|
context.getString(R.string.contact),
|
||||||
third = {
|
third = {
|
||||||
context.openUrl("mailto:$it")
|
context.openUrl("mailto:$it")
|
||||||
}
|
}
|
||||||
@@ -134,7 +131,7 @@ fun AboutSettingsScreen(
|
|||||||
stringResource(R.string.contributors_description),
|
stringResource(R.string.contributors_description),
|
||||||
third = nav@{
|
third = nav@{
|
||||||
if (!viewModel.isConnected) {
|
if (!viewModel.isConnected) {
|
||||||
context.toast(resources.getString(R.string.no_network_toast))
|
context.toast(context.getString(R.string.no_network_toast))
|
||||||
return@nav
|
return@nav
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +153,7 @@ fun AboutSettingsScreen(
|
|||||||
LaunchedEffect(developerTaps) {
|
LaunchedEffect(developerTaps) {
|
||||||
if (developerTaps == 0) return@LaunchedEffect
|
if (developerTaps == 0) return@LaunchedEffect
|
||||||
if (showDeveloperSettings) {
|
if (showDeveloperSettings) {
|
||||||
snackbarHostState.showSnackbar(resources.getString(R.string.developer_options_already_enabled))
|
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_already_enabled))
|
||||||
developerTaps = 0
|
developerTaps = 0
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
@@ -164,7 +161,7 @@ fun AboutSettingsScreen(
|
|||||||
val remaining = DEVELOPER_OPTIONS_TAPS - developerTaps
|
val remaining = DEVELOPER_OPTIONS_TAPS - developerTaps
|
||||||
if (remaining > 0) {
|
if (remaining > 0) {
|
||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
resources.getString(
|
context.getString(
|
||||||
R.string.developer_options_taps,
|
R.string.developer_options_taps,
|
||||||
remaining
|
remaining
|
||||||
),
|
),
|
||||||
@@ -172,7 +169,7 @@ fun AboutSettingsScreen(
|
|||||||
)
|
)
|
||||||
} else if (remaining == 0) {
|
} else if (remaining == 0) {
|
||||||
viewModel.showDeveloperSettings.update(true)
|
viewModel.showDeveloperSettings.update(true)
|
||||||
snackbarHostState.showSnackbar(resources.getString(R.string.developer_options_enabled))
|
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_enabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the counter
|
// Reset the counter
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalResources
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -65,10 +64,9 @@ fun AdvancedSettingsScreen(
|
|||||||
viewModel: AdvancedSettingsViewModel = koinViewModel()
|
viewModel: AdvancedSettingsViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val resources = LocalResources.current
|
val memoryLimit = remember {
|
||||||
val memoryLimit = remember(resources) {
|
|
||||||
val activityManager = context.getSystemService<ActivityManager>()!!
|
val activityManager = context.getSystemService<ActivityManager>()!!
|
||||||
resources.getString(
|
context.getString(
|
||||||
R.string.device_memory_limit_format,
|
R.string.device_memory_limit_format,
|
||||||
activityManager.memoryClass,
|
activityManager.memoryClass,
|
||||||
activityManager.largeMemoryClass
|
activityManager.largeMemoryClass
|
||||||
@@ -185,7 +183,7 @@ fun AdvancedSettingsScreen(
|
|||||||
ClipData.newPlainText("Device Information", deviceContent)
|
ClipData.newPlainText("Device Information", deviceContent)
|
||||||
)
|
)
|
||||||
|
|
||||||
context.toast(resources.getString(R.string.toast_copied_to_clipboard))
|
context.toast(context.getString(R.string.toast_copied_to_clipboard))
|
||||||
}.withHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
}.withHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
),
|
),
|
||||||
headlineContent = stringResource(R.string.about_device),
|
headlineContent = stringResource(R.string.about_device),
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
@@ -21,7 +19,6 @@ import androidx.compose.material3.rememberTopAppBarState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -41,7 +38,6 @@ import app.revanced.manager.ui.theme.Theme
|
|||||||
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
|
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -52,7 +48,6 @@ fun GeneralSettingsScreen(
|
|||||||
val prefs = viewModel.prefs
|
val prefs = viewModel.prefs
|
||||||
val coroutineScope = viewModel.viewModelScope
|
val coroutineScope = viewModel.viewModelScope
|
||||||
var showThemePicker by rememberSaveable { mutableStateOf(false) }
|
var showThemePicker by rememberSaveable { mutableStateOf(false) }
|
||||||
var showLanguagePicker by rememberSaveable { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (showThemePicker) {
|
if (showThemePicker) {
|
||||||
ThemePicker(
|
ThemePicker(
|
||||||
@@ -60,17 +55,6 @@ fun GeneralSettingsScreen(
|
|||||||
onConfirm = { viewModel.setTheme(it) }
|
onConfirm = { viewModel.setTheme(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showLanguagePicker) {
|
|
||||||
LanguagePicker(
|
|
||||||
supportedLocales = viewModel.getSupportedLocales(),
|
|
||||||
currentLocale = viewModel.getCurrentLocale(),
|
|
||||||
onDismiss = { showLanguagePicker = false },
|
|
||||||
onConfirm = { viewModel.setLocale(it) },
|
|
||||||
getDisplayName = { viewModel.getLocaleDisplayName(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -90,24 +74,6 @@ fun GeneralSettingsScreen(
|
|||||||
) {
|
) {
|
||||||
GroupHeader(stringResource(R.string.appearance))
|
GroupHeader(stringResource(R.string.appearance))
|
||||||
|
|
||||||
val currentLocale = viewModel.getCurrentLocale()
|
|
||||||
val currentLanguageDisplay = remember(currentLocale) {
|
|
||||||
currentLocale?.let { viewModel.getLocaleDisplayName(it) }
|
|
||||||
}
|
|
||||||
SettingsListItem(
|
|
||||||
modifier = Modifier.clickable { showLanguagePicker = true },
|
|
||||||
headlineContent = stringResource(R.string.language),
|
|
||||||
supportingContent = stringResource(R.string.language_description),
|
|
||||||
trailingContent = {
|
|
||||||
FilledTonalButton(onClick = { showLanguagePicker = true }) {
|
|
||||||
Text(
|
|
||||||
currentLanguageDisplay
|
|
||||||
?: stringResource(R.string.language_system_default)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val theme by prefs.theme.getAsState()
|
val theme by prefs.theme.getAsState()
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable { showThemePicker = true },
|
modifier = Modifier.clickable { showThemePicker = true },
|
||||||
@@ -139,14 +105,6 @@ fun GeneralSettingsScreen(
|
|||||||
description = R.string.pure_black_theme_description
|
description = R.string.pure_black_theme_description
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.networking))
|
|
||||||
BooleanItem(
|
|
||||||
preference = prefs.allowMeteredNetworks,
|
|
||||||
coroutineScope = coroutineScope,
|
|
||||||
headline = R.string.allow_metered_networks,
|
|
||||||
description = R.string.allow_metered_networks_description
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,64 +148,4 @@ private fun ThemePicker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LanguagePicker(
|
|
||||||
supportedLocales: List<Locale>,
|
|
||||||
currentLocale: Locale?,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onConfirm: (Locale?) -> Unit,
|
|
||||||
getDisplayName: (Locale) -> String
|
|
||||||
) {
|
|
||||||
var selectedLocale by remember { mutableStateOf(currentLocale) }
|
|
||||||
val systemDefaultString = stringResource(R.string.language_system_default)
|
|
||||||
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
title = { Text(stringResource(R.string.language)) },
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { selectedLocale = null },
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
HapticRadioButton(
|
|
||||||
selected = selectedLocale == null,
|
|
||||||
onClick = { selectedLocale = null }
|
|
||||||
)
|
|
||||||
Text(systemDefaultString)
|
|
||||||
}
|
|
||||||
|
|
||||||
supportedLocales.forEach { locale ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { selectedLocale = locale },
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
HapticRadioButton(
|
|
||||||
selected = selectedLocale == locale,
|
|
||||||
onClick = { selectedLocale = locale }
|
|
||||||
)
|
|
||||||
Text(getDisplayName(locale))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
onConfirm(selectedLocale)
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.apply))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalResources
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -65,7 +64,6 @@ fun ImportExportSettingsScreen(
|
|||||||
vm: ImportExportViewModel = koinViewModel()
|
vm: ImportExportViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val resources = LocalResources.current
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var selectorDialog by rememberSaveable { mutableStateOf<(@Composable () -> Unit)?>(null) }
|
var selectorDialog by rememberSaveable { mutableStateOf<(@Composable () -> Unit)?>(null) }
|
||||||
|
|
||||||
@@ -110,7 +108,7 @@ fun ImportExportSettingsScreen(
|
|||||||
vm.viewModelScope.launch {
|
vm.viewModelScope.launch {
|
||||||
uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") {
|
uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") {
|
||||||
val result = vm.tryKeystoreImport(alias, pass)
|
val result = vm.tryKeystoreImport(alias, pass)
|
||||||
if (!result) context.toast(resources.getString(R.string.import_keystore_wrong_credentials))
|
if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,7 +166,7 @@ fun ImportExportSettingsScreen(
|
|||||||
GroupItem(
|
GroupItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!vm.canExport()) {
|
if (!vm.canExport()) {
|
||||||
context.toast(resources.getString(R.string.export_keystore_unavailable))
|
context.toast(context.getString(R.string.export_keystore_unavailable))
|
||||||
return@GroupItem
|
return@GroupItem
|
||||||
}
|
}
|
||||||
exportKeystoreLauncher.launch("Manager.keystore")
|
exportKeystoreLauncher.launch("Manager.keystore")
|
||||||
|
|||||||
@@ -7,18 +7,15 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppScaffold
|
import app.revanced.manager.ui.component.AppScaffold
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.Scrollbar
|
import app.revanced.manager.ui.component.Scrollbar
|
||||||
|
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
import com.mikepenz.aboutlibraries.ui.compose.libraryColors
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.m3.chipColors
|
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.m3.libraryColors
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -36,23 +33,16 @@ fun LicensesSettingsScreen(
|
|||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(modifier = Modifier.padding(paddingValues)) {
|
Column(modifier = Modifier.padding(paddingValues)) {
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
val libraries by produceLibraries(R.raw.aboutlibraries)
|
|
||||||
val chipColors = LibraryDefaults.chipColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
)
|
|
||||||
|
|
||||||
LibrariesContainer(
|
LibrariesContainer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
libraries = libraries,
|
|
||||||
lazyListState = lazyListState,
|
lazyListState = lazyListState,
|
||||||
colors = LibraryDefaults.libraryColors(
|
colors = LibraryDefaults.libraryColors(
|
||||||
libraryBackgroundColor = MaterialTheme.colorScheme.background,
|
backgroundColor = MaterialTheme.colorScheme.background,
|
||||||
libraryContentColor = MaterialTheme.colorScheme.onBackground,
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
versionChipColors = chipColors,
|
badgeBackgroundColor = MaterialTheme.colorScheme.primary,
|
||||||
licenseChipColors = chipColors,
|
badgeContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
fundingChipColors = chipColors,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Scrollbar(lazyListState = lazyListState, modifier = Modifier.padding(paddingValues))
|
Scrollbar(lazyListState = lazyListState, modifier = Modifier.padding(paddingValues))
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalResources
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
@@ -34,7 +33,6 @@ fun UpdatesSettingsScreen(
|
|||||||
vm: UpdatesSettingsViewModel = koinViewModel(),
|
vm: UpdatesSettingsViewModel = koinViewModel(),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val resources = LocalResources.current
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
@@ -59,7 +57,7 @@ fun UpdatesSettingsScreen(
|
|||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
if (!vm.isConnected) {
|
if (!vm.isConnected) {
|
||||||
context.toast(resources.getString(R.string.no_network_toast))
|
context.toast(context.getString(R.string.no_network_toast))
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
if (vm.checkForUpdates()) onUpdateClick()
|
if (vm.checkForUpdates()) onUpdateClick()
|
||||||
@@ -72,7 +70,7 @@ fun UpdatesSettingsScreen(
|
|||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
if (!vm.isConnected) {
|
if (!vm.isConnected) {
|
||||||
context.toast(resources.getString(R.string.no_network_toast))
|
context.toast(context.getString(R.string.no_network_toast))
|
||||||
return@clickable
|
return@clickable
|
||||||
}
|
}
|
||||||
onChangelogClick()
|
onChangelogClick()
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ package app.revanced.manager.ui.viewmodel
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -14,7 +11,6 @@ import androidx.lifecycle.viewmodel.compose.saveable
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.platform.Filesystem
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -31,7 +27,7 @@ class AppSelectorViewModel(
|
|||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val pm: PM,
|
private val pm: PM,
|
||||||
fs: Filesystem,
|
fs: Filesystem,
|
||||||
private val patchBundleRepository: PatchBundleRepository,
|
patchBundleRepository: PatchBundleRepository,
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val inputFile = savedStateHandle.saveable(key = "inputFile") {
|
private val inputFile = savedStateHandle.saveable(key = "inputFile") {
|
||||||
@@ -42,19 +38,19 @@ class AppSelectorViewModel(
|
|||||||
}
|
}
|
||||||
val appList = pm.appList
|
val appList = pm.appList
|
||||||
|
|
||||||
private val storageSelectionChannel = Channel<SelectedApp.Local>()
|
private val storageSelectionChannel = Channel<Pair<String, String>>()
|
||||||
val storageSelectionFlow = storageSelectionChannel.receiveAsFlow()
|
val storageSelectionFlow = storageSelectionChannel.receiveAsFlow()
|
||||||
|
|
||||||
val suggestedAppVersions = patchBundleRepository.suggestedVersions.flowOn(Dispatchers.Default)
|
val suggestedAppVersions = patchBundleRepository.suggestedVersions.flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null)
|
// var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null)
|
||||||
private set
|
// private set
|
||||||
|
|
||||||
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
|
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
|
||||||
|
|
||||||
fun dismissNonSuggestedVersionDialog() {
|
// fun dismissNonSuggestedVersionDialog() {
|
||||||
nonSuggestedVersionDialogSubject = null
|
// nonSuggestedVersionDialogSubject = null
|
||||||
}
|
// }
|
||||||
|
|
||||||
fun handleStorageResult(uri: Uri) = viewModelScope.launch {
|
fun handleStorageResult(uri: Uri) = viewModelScope.launch {
|
||||||
val selectedApp = withContext(Dispatchers.IO) {
|
val selectedApp = withContext(Dispatchers.IO) {
|
||||||
@@ -66,11 +62,8 @@ class AppSelectorViewModel(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patchBundleRepository.isVersionAllowed(selectedApp.packageName, selectedApp.version)) {
|
// TODO: Disallow if 0 patches are compatible
|
||||||
storageSelectionChannel.send(selectedApp)
|
storageSelectionChannel.send(selectedApp)
|
||||||
} else {
|
|
||||||
nonSuggestedVersionDialogSubject = selectedApp
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSelectedFile(uri: Uri) =
|
private fun loadSelectedFile(uri: Uri) =
|
||||||
@@ -80,12 +73,7 @@ class AppSelectorViewModel(
|
|||||||
Files.copy(stream, toPath())
|
Files.copy(stream, toPath())
|
||||||
|
|
||||||
pm.getPackageInfo(this)?.let { packageInfo ->
|
pm.getPackageInfo(this)?.let { packageInfo ->
|
||||||
SelectedApp.Local(
|
Pair(packageInfo.packageName, path)
|
||||||
packageName = packageInfo.packageName,
|
|
||||||
version = packageInfo.versionName!!,
|
|
||||||
file = this,
|
|
||||||
temporary = true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ class BundleListViewModel : ViewModel(), KoinComponent {
|
|||||||
patchBundleRepository.update(
|
patchBundleRepository.update(
|
||||||
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
|
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
|
||||||
showToast = true,
|
showToast = true,
|
||||||
force = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +65,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
|
|||||||
fun update(src: PatchBundleSource) = viewModelScope.launch {
|
fun update(src: PatchBundleSource) = viewModelScope.launch {
|
||||||
if (src !is RemotePatchBundle) return@launch
|
if (src !is RemotePatchBundle) return@launch
|
||||||
|
|
||||||
patchBundleRepository.update(src, showToast = true, force = true)
|
patchBundleRepository.update(src, showToast = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Event {
|
enum class Event {
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.ui.theme.Theme
|
import app.revanced.manager.ui.theme.Theme
|
||||||
import app.revanced.manager.util.SupportedLocales
|
|
||||||
import app.revanced.manager.util.resetListItemColorsCached
|
import app.revanced.manager.util.resetListItemColorsCached
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class GeneralSettingsViewModel(
|
class GeneralSettingsViewModel(
|
||||||
private val app: Application,
|
|
||||||
val prefs: PreferencesManager
|
val prefs: PreferencesManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
fun setTheme(theme: Theme) = viewModelScope.launch {
|
fun setTheme(theme: Theme) = viewModelScope.launch {
|
||||||
prefs.theme.update(theme)
|
prefs.theme.update(theme)
|
||||||
resetListItemColorsCached()
|
resetListItemColorsCached()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSupportedLocales() = SupportedLocales.getSupportedLocales(app)
|
|
||||||
fun getCurrentLocale() = SupportedLocales.getCurrentLocale()
|
|
||||||
fun setLocale(locale: Locale?) = SupportedLocales.setLocale(locale)
|
|
||||||
fun getLocaleDisplayName(locale: Locale) = SupportedLocales.getDisplayName(locale)
|
|
||||||
}
|
}
|
||||||
@@ -36,8 +36,8 @@ import kotlin.io.path.deleteExisting
|
|||||||
import kotlin.io.path.inputStream
|
import kotlin.io.path.inputStream
|
||||||
|
|
||||||
sealed class ResetDialogState(
|
sealed class ResetDialogState(
|
||||||
@param:StringRes val titleResId: Int,
|
@StringRes val titleResId: Int,
|
||||||
@param:StringRes val descriptionResId: Int,
|
@StringRes val descriptionResId: Int,
|
||||||
val onConfirm: () -> Unit,
|
val onConfirm: () -> Unit,
|
||||||
val dialogOptionName: String? = null
|
val dialogOptionName: String? = null
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -12,11 +12,9 @@ import app.revanced.manager.R
|
|||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||||
import app.revanced.manager.domain.manager.KeystoreManager
|
import app.revanced.manager.domain.manager.KeystoreManager
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||||
import app.revanced.manager.domain.repository.SerializedSelection
|
import app.revanced.manager.domain.repository.SerializedSelection
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
|
||||||
import app.revanced.manager.ui.theme.Theme
|
import app.revanced.manager.ui.theme.Theme
|
||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
@@ -31,44 +29,14 @@ import kotlinx.serialization.json.Json
|
|||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
private val patchBundleRepository: PatchBundleRepository,
|
private val patchBundleRepository: PatchBundleRepository,
|
||||||
private val patchSelectionRepository: PatchSelectionRepository,
|
private val patchSelectionRepository: PatchSelectionRepository,
|
||||||
private val downloadedAppRepository: DownloadedAppRepository,
|
|
||||||
private val keystoreManager: KeystoreManager,
|
private val keystoreManager: KeystoreManager,
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
val prefs: PreferencesManager,
|
val prefs: PreferencesManager,
|
||||||
private val json: Json
|
private val json: Json
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val appSelectChannel = Channel<SelectedApp>()
|
|
||||||
val appSelectFlow = appSelectChannel.receiveAsFlow()
|
|
||||||
private val legacyImportActivityChannel = Channel<Intent>()
|
private val legacyImportActivityChannel = Channel<Intent>()
|
||||||
val legacyImportActivityFlow = legacyImportActivityChannel.receiveAsFlow()
|
val legacyImportActivityFlow = legacyImportActivityChannel.receiveAsFlow()
|
||||||
|
|
||||||
private suspend fun suggestedVersion(packageName: String) =
|
|
||||||
patchBundleRepository.suggestedVersions.first()[packageName]
|
|
||||||
|
|
||||||
private suspend fun findDownloadedApp(app: SelectedApp): SelectedApp.Local? {
|
|
||||||
if (app !is SelectedApp.Search) return null
|
|
||||||
|
|
||||||
val suggestedVersion = suggestedVersion(app.packageName) ?: return null
|
|
||||||
|
|
||||||
val downloadedApp =
|
|
||||||
downloadedAppRepository.get(app.packageName, suggestedVersion, markUsed = true)
|
|
||||||
?: return null
|
|
||||||
return SelectedApp.Local(
|
|
||||||
downloadedApp.packageName,
|
|
||||||
downloadedApp.version,
|
|
||||||
downloadedAppRepository.getApkFileForApp(downloadedApp),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun selectApp(app: SelectedApp) = viewModelScope.launch {
|
|
||||||
appSelectChannel.send(findDownloadedApp(app) ?: app)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun selectApp(packageName: String) = viewModelScope.launch {
|
|
||||||
selectApp(SelectedApp.Search(packageName, suggestedVersion(packageName)))
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (!prefs.firstLaunch.get()) return@launch
|
if (!prefs.firstLaunch.get()) return@launch
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import app.revanced.manager.patcher.worker.PatcherWorker
|
|||||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||||
import app.revanced.manager.ui.model.InstallerModel
|
import app.revanced.manager.ui.model.InstallerModel
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedSource
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.ui.model.State
|
||||||
import app.revanced.manager.ui.model.StepCategory
|
import app.revanced.manager.ui.model.StepCategory
|
||||||
import app.revanced.manager.ui.model.Step
|
import app.revanced.manager.ui.model.Step
|
||||||
@@ -93,9 +93,8 @@ class PatcherViewModel(
|
|||||||
private val ackpineInstaller: PackageInstaller = get()
|
private val ackpineInstaller: PackageInstaller = get()
|
||||||
|
|
||||||
private var installedApp: InstalledApp? = null
|
private var installedApp: InstalledApp? = null
|
||||||
private val selectedApp = input.selectedApp
|
val packageName = input.packageName
|
||||||
val packageName = selectedApp.packageName
|
val version = input.version
|
||||||
val version = selectedApp.version
|
|
||||||
|
|
||||||
var installedPackageName by savedStateHandle.saveable(
|
var installedPackageName by savedStateHandle.saveable(
|
||||||
key = "installedPackageName",
|
key = "installedPackageName",
|
||||||
@@ -160,7 +159,7 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
||||||
generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList()
|
generateSteps(app, input.selectedSource, input.selectedPatches).toMutableStateList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val progress by derivedStateOf {
|
val progress by derivedStateOf {
|
||||||
@@ -178,7 +177,9 @@ class PatcherViewModel(
|
|||||||
ParcelUuid(
|
ParcelUuid(
|
||||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||||
"patching", PatcherWorker.Args(
|
"patching", PatcherWorker.Args(
|
||||||
input.selectedApp,
|
input.packageName,
|
||||||
|
input.version,
|
||||||
|
input.selectedSource,
|
||||||
outputFile.path,
|
outputFile.path,
|
||||||
input.selectedPatches,
|
input.selectedPatches,
|
||||||
input.options,
|
input.options,
|
||||||
@@ -257,7 +258,7 @@ class PatcherViewModel(
|
|||||||
super.onCleared()
|
super.onCleared()
|
||||||
workManager.cancelWorkById(patcherWorkerId.uuid)
|
workManager.cancelWorkById(patcherWorkerId.uuid)
|
||||||
|
|
||||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
if (input.selectedSource is SelectedSource.Installed && installedApp?.installType == InstallType.MOUNT) {
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
|
uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
|
||||||
withTimeout(Duration.ofMinutes(1L)) {
|
withTimeout(Duration.ofMinutes(1L)) {
|
||||||
@@ -381,7 +382,7 @@ class PatcherViewModel(
|
|||||||
installedAppRepository.addOrUpdate(
|
installedAppRepository.addOrUpdate(
|
||||||
installerPkgName,
|
installerPkgName,
|
||||||
packageName,
|
packageName,
|
||||||
input.selectedApp.version
|
input.version
|
||||||
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
|
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
|
||||||
InstallType.DEFAULT,
|
InstallType.DEFAULT,
|
||||||
input.selectedPatches
|
input.selectedPatches
|
||||||
@@ -443,7 +444,7 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputVersion = input.selectedApp.version
|
val inputVersion = input.version
|
||||||
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
||||||
?: throw Exception("Failed to determine input APK version")
|
?: throw Exception("Failed to determine input APK version")
|
||||||
|
|
||||||
@@ -535,10 +536,10 @@ class PatcherViewModel(
|
|||||||
|
|
||||||
fun generateSteps(
|
fun generateSteps(
|
||||||
context: Context,
|
context: Context,
|
||||||
selectedApp: SelectedApp,
|
selectedSource: SelectedSource,
|
||||||
selectedPatches: PatchSelection
|
selectedPatches: PatchSelection
|
||||||
): List<Step> = buildList {
|
): List<Step> = buildList {
|
||||||
if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search)
|
if (selectedSource is SelectedSource.Plugin)
|
||||||
add(
|
add(
|
||||||
Step(
|
Step(
|
||||||
StepId.DownloadAPK,
|
StepId.DownloadAPK,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import app.revanced.manager.domain.repository.PatchBundleRepository
|
|||||||
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.saver.Nullable
|
import app.revanced.manager.util.saver.Nullable
|
||||||
@@ -45,14 +45,14 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
|
||||||
@OptIn(SavedStateHandleSaveableApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class)
|
||||||
class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) :
|
class PatchesSelectorViewModel(input: SelectedAppInfo.PatchesSelector.ViewModelParams) :
|
||||||
ViewModel(), KoinComponent {
|
ViewModel(), KoinComponent {
|
||||||
private val app: Application = get()
|
private val app: Application = get()
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
private val prefs: PreferencesManager = get()
|
private val prefs: PreferencesManager = get()
|
||||||
|
|
||||||
private val packageName = input.app.packageName
|
private val packageName = input.packageName
|
||||||
val appVersion = input.app.version
|
val appVersion = input.version
|
||||||
|
|
||||||
var selectionWarningEnabled by mutableStateOf(true)
|
var selectionWarningEnabled by mutableStateOf(true)
|
||||||
private set
|
private set
|
||||||
@@ -62,7 +62,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
|||||||
val allowIncompatiblePatches =
|
val allowIncompatiblePatches =
|
||||||
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
|
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
|
||||||
val bundlesFlow =
|
val bundlesFlow =
|
||||||
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.app.version)
|
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.version)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -88,7 +88,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
|||||||
key = "selection",
|
key = "selection",
|
||||||
stateSaver = selectionSaver,
|
stateSaver = selectionSaver,
|
||||||
) {
|
) {
|
||||||
mutableStateOf(input.currentSelection?.toPersistentPatchSelection())
|
mutableStateOf(input.patchSelection?.toPersistentPatchSelection())
|
||||||
}
|
}
|
||||||
|
|
||||||
private val patchOptions: PersistentOptions by savedStateHandle.saveable(
|
private val patchOptions: PersistentOptions by savedStateHandle.saveable(
|
||||||
|
|||||||
@@ -1,130 +1,188 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.result.ActivityResult
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||||
import androidx.lifecycle.viewmodel.compose.saveable
|
import androidx.lifecycle.viewmodel.compose.saveable
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
|
||||||
import app.revanced.manager.domain.installer.RootInstaller
|
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||||
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||||
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
|
||||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
|
||||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
||||||
import app.revanced.manager.plugin.downloader.GetScope
|
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
import app.revanced.manager.ui.model.SelectedSource
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedVersion
|
||||||
import app.revanced.manager.ui.model.navigation.Patcher
|
import app.revanced.manager.ui.model.navigation.Patcher
|
||||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.simpleMessage
|
import app.revanced.manager.util.patchCount
|
||||||
import app.revanced.manager.util.tag
|
|
||||||
import app.revanced.manager.util.toast
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||||
class SelectedAppInfoViewModel(
|
class SelectedAppInfoViewModel(
|
||||||
input: SelectedApplicationInfo.ViewModelParams
|
private val input: SelectedAppInfo.ViewModelParams
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
private val app: Application = get()
|
|
||||||
private val bundleRepository: PatchBundleRepository = get()
|
private val bundleRepository: PatchBundleRepository = get()
|
||||||
private val selectionRepository: PatchSelectionRepository = get()
|
private val selectionRepository: PatchSelectionRepository = get()
|
||||||
private val optionsRepository: PatchOptionsRepository = get()
|
private val optionsRepository: PatchOptionsRepository = get()
|
||||||
private val pluginsRepository: DownloaderPluginRepository = get()
|
private val pluginsRepository: DownloaderPluginRepository = get()
|
||||||
private val installedAppRepository: InstalledAppRepository = get()
|
private val installedAppRepository: InstalledAppRepository = get()
|
||||||
private val rootInstaller: RootInstaller = get()
|
private val downloadedAppRepository: DownloadedAppRepository = get()
|
||||||
private val pm: PM = get()
|
private val pm: PM = get()
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
val prefs: PreferencesManager = get()
|
private val prefs: PreferencesManager = get()
|
||||||
val plugins = pluginsRepository.loadedPluginsFlow
|
val plugins = pluginsRepository.loadedPluginsFlow
|
||||||
val desiredVersion = input.app.version
|
val packageName = input.packageName
|
||||||
val packageName = input.app.packageName
|
val localPath = input.localPath
|
||||||
|
|
||||||
private val persistConfiguration = input.patches == null
|
private val persistConfiguration = input.patches == null
|
||||||
|
|
||||||
val hasRoot = rootInstaller.hasRootAccess()
|
|
||||||
var installedAppData: Pair<SelectedApp.Installed, InstalledApp?>? by mutableStateOf(null)
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var _selectedApp by savedStateHandle.saveable {
|
// User selection
|
||||||
mutableStateOf(input.app)
|
private var selectionFlow = MutableStateFlow(
|
||||||
|
input.patches?.let { selection ->
|
||||||
|
SelectionState.Customized(selection)
|
||||||
|
} ?: SelectionState.Default
|
||||||
|
)
|
||||||
|
|
||||||
|
private val _selectedVersion = MutableStateFlow<SelectedVersion>(SelectedVersion.Auto)
|
||||||
|
val selectedVersion: StateFlow<SelectedVersion> = _selectedVersion
|
||||||
|
|
||||||
|
private val _selectedSource = MutableStateFlow<SelectedSource>(SelectedSource.Auto)
|
||||||
|
val selectedSource: StateFlow<SelectedSource> = _selectedSource
|
||||||
|
|
||||||
|
fun updateVersion(version: SelectedVersion) {
|
||||||
|
_selectedVersion.value = version
|
||||||
|
}
|
||||||
|
fun updateSource(source: SelectedSource) {
|
||||||
|
_selectedSource.value = source
|
||||||
|
}
|
||||||
|
fun updateConfiguration(
|
||||||
|
selection: PatchSelection?,
|
||||||
|
selectedOptions: Options
|
||||||
|
) = viewModelScope.launch {
|
||||||
|
selectionFlow.value = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
||||||
|
|
||||||
|
val filteredOptions = selectedOptions.filtered(bundleInfoFlow.first())
|
||||||
|
options = filteredOptions
|
||||||
|
|
||||||
|
if (persistConfiguration) {
|
||||||
|
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
||||||
|
?: selectionRepository.resetSelectionForPackage(packageName)
|
||||||
|
|
||||||
|
optionsRepository.saveOptions(packageName, filteredOptions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedAppInfo: PackageInfo? by mutableStateOf(null)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var selectedApp
|
// All patches for package
|
||||||
get() = _selectedApp
|
val bundles = bundleRepository.scopedBundleInfoFlow(packageName, null)
|
||||||
set(value) {
|
|
||||||
_selectedApp = value
|
// Selection derived from selectionFlow
|
||||||
invalidateSelectedAppInfo()
|
val patchSelection = combine(
|
||||||
|
selectionFlow,
|
||||||
|
bundles,
|
||||||
|
) { selection, bundles ->
|
||||||
|
selection.patches(bundles, allowIncompatible = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val customSelection = combine(
|
||||||
|
selectionFlow,
|
||||||
|
bundles,
|
||||||
|
) { selection, bundles ->
|
||||||
|
(selection as? SelectionState.Customized)?.patches(bundles, allowIncompatible = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Most compatible versions based on patch selection
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val mostCompatibleVersions = patchSelection.flatMapLatest { patchSelection ->
|
||||||
|
bundleRepository.suggestedVersions(
|
||||||
|
packageName,
|
||||||
|
patchSelection
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve actual version from user selection
|
||||||
|
val resolvedVersion = combine(
|
||||||
|
_selectedVersion,
|
||||||
|
mostCompatibleVersions,
|
||||||
|
) { selected, mostCompatible ->
|
||||||
|
when (selected) {
|
||||||
|
is SelectedVersion.Specific -> selected.version
|
||||||
|
is SelectedVersion.Any -> null
|
||||||
|
is SelectedVersion.Auto -> mostCompatible?.maxWithOrNull(
|
||||||
|
compareBy<Map.Entry<String, Int>> { it.value }
|
||||||
|
.thenBy { it.key }
|
||||||
|
)?.key
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
invalidateSelectedAppInfo()
|
val scopedBundles = resolvedVersion.flatMapLatest { version ->
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
bundleRepository.scopedBundleInfoFlow(packageName, version)
|
||||||
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
|
}
|
||||||
val installedAppDeferred =
|
|
||||||
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
|
|
||||||
|
|
||||||
installedAppData =
|
val incompatiblePatchCount = scopedBundles.map { bundles ->
|
||||||
packageInfo.await()?.let {
|
bundles.sumOf { bundle ->
|
||||||
SelectedApp.Installed(
|
bundle.incompatible.size
|
||||||
packageName,
|
}
|
||||||
it.versionName!!
|
}
|
||||||
) to installedAppDeferred.await()
|
|
||||||
|
// Resolve actual source from user selection
|
||||||
|
val resolvedSource = combine(
|
||||||
|
_selectedSource,
|
||||||
|
resolvedVersion
|
||||||
|
) { source, version ->
|
||||||
|
when (source) {
|
||||||
|
is SelectedSource.Installed -> source
|
||||||
|
is SelectedSource.Local -> source
|
||||||
|
is SelectedSource.Downloaded -> source
|
||||||
|
is SelectedSource.Plugin -> source
|
||||||
|
is SelectedSource.Auto -> {
|
||||||
|
val app = version?.let {
|
||||||
|
downloadedAppRepository.get(packageName, it)
|
||||||
}
|
}
|
||||||
|
val file = app?.let {
|
||||||
|
downloadedAppRepository.getApkFileForApp(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
file?.let { SelectedSource.Downloaded(it.path, version) }
|
||||||
|
?: SelectedSource.Plugin(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val requiredVersion = combine(
|
|
||||||
prefs.suggestedVersionSafeguard.flow,
|
|
||||||
bundleRepository.suggestedVersions
|
|
||||||
) { suggestedVersionSafeguard, suggestedVersions ->
|
|
||||||
if (!suggestedVersionSafeguard) return@combine null
|
|
||||||
|
|
||||||
suggestedVersions[input.app.packageName]
|
|
||||||
}
|
|
||||||
|
|
||||||
val bundleInfoFlow by derivedStateOf {
|
val bundleInfoFlow by derivedStateOf {
|
||||||
bundleRepository.scopedBundleInfoFlow(packageName, selectedApp.version)
|
bundleRepository.scopedBundleInfoFlow(packageName, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
var options: Options by savedStateHandle.saveable {
|
var options: Options by savedStateHandle.saveable {
|
||||||
@@ -142,121 +200,42 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var selectionState: SelectionState by savedStateHandle.saveable {
|
|
||||||
if (input.patches != null)
|
|
||||||
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
|
|
||||||
|
|
||||||
// Try to get the previous selection if customization is enabled.
|
val errorFlow = combine(
|
||||||
viewModelScope.launch {
|
plugins,
|
||||||
if (!prefs.disableSelectionWarning.get()) return@launch
|
resolvedSource,
|
||||||
|
) { pluginsList, source ->
|
||||||
val previous = selectionRepository.getSelection(packageName)
|
|
||||||
if (previous.values.sumOf { it.size } == 0) return@launch
|
|
||||||
selectionState = SelectionState.Customized(previous)
|
|
||||||
}
|
|
||||||
|
|
||||||
mutableStateOf(SelectionState.Default)
|
|
||||||
}
|
|
||||||
|
|
||||||
var showSourceSelector by mutableStateOf(false)
|
|
||||||
private set
|
|
||||||
private var pluginAction: Pair<LoadedDownloaderPlugin, Job>? by mutableStateOf(null)
|
|
||||||
val activePluginAction get() = pluginAction?.first?.packageName
|
|
||||||
private var launchedActivity by mutableStateOf<CompletableDeferred<ActivityResult>?>(null)
|
|
||||||
private val launchActivityChannel = Channel<Intent>()
|
|
||||||
val launchActivityFlow = launchActivityChannel.receiveAsFlow()
|
|
||||||
|
|
||||||
val errorFlow = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app ->
|
|
||||||
when {
|
when {
|
||||||
app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins
|
source is SelectedSource.Plugin && pluginsList.isEmpty() -> Error.NoPlugins
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSourceSelector() {
|
|
||||||
dismissSourceSelector()
|
|
||||||
showSourceSelector = true
|
// var installedAppData: Pair<SelectedApp.Installed, InstalledApp?>? by mutableStateOf(null)
|
||||||
|
// private set
|
||||||
|
|
||||||
|
private var _selectedApp by savedStateHandle.saveable {
|
||||||
|
mutableStateOf(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelPluginAction() {
|
var selectedAppInfo: PackageInfo? by mutableStateOf(null)
|
||||||
pluginAction?.second?.cancel()
|
private set
|
||||||
pluginAction = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dismissSourceSelector() {
|
var selectedApp
|
||||||
cancelPluginAction()
|
get() = _selectedApp
|
||||||
showSourceSelector = false
|
set(value) {
|
||||||
}
|
_selectedApp = value
|
||||||
|
invalidateSelectedAppInfo()
|
||||||
fun searchUsingPlugin(plugin: LoadedDownloaderPlugin) {
|
|
||||||
cancelPluginAction()
|
|
||||||
pluginAction = plugin to viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
val scope = object : GetScope {
|
|
||||||
override val hostPackageName = app.packageName
|
|
||||||
override val pluginPackageName = plugin.packageName
|
|
||||||
override suspend fun requestStartActivity(intent: Intent) =
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (launchedActivity != null) error("Previous activity has not finished")
|
|
||||||
try {
|
|
||||||
val result = with(CompletableDeferred<ActivityResult>()) {
|
|
||||||
launchedActivity = this
|
|
||||||
launchActivityChannel.send(intent)
|
|
||||||
await()
|
|
||||||
}
|
|
||||||
when (result.resultCode) {
|
|
||||||
Activity.RESULT_OK -> result.data
|
|
||||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
|
||||||
else -> throw UserInteractionException.Activity.NotCompleted(
|
|
||||||
result.resultCode,
|
|
||||||
result.data
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
launchedActivity = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
plugin.get(scope, packageName, desiredVersion)
|
|
||||||
}?.let { (data, version) ->
|
|
||||||
if (desiredVersion != null && version != desiredVersion) {
|
|
||||||
app.toast(app.getString(R.string.downloader_invalid_version))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
selectedApp = SelectedApp.Download(
|
|
||||||
packageName,
|
|
||||||
version,
|
|
||||||
ParceledDownloaderData(plugin, data)
|
|
||||||
)
|
|
||||||
} ?: app.toast(app.getString(R.string.downloader_app_not_found))
|
|
||||||
} catch (e: UserInteractionException.Activity) {
|
|
||||||
app.toast(e.message!!)
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
app.toast(app.getString(R.string.downloader_error, e.simpleMessage()))
|
|
||||||
Log.e(tag, "Downloader.get threw an exception", e)
|
|
||||||
} finally {
|
|
||||||
pluginAction = null
|
|
||||||
dismissSourceSelector()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun handlePluginActivityResult(result: ActivityResult) {
|
|
||||||
launchedActivity?.complete(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: Load from local file or downloaded app
|
||||||
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
|
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
|
||||||
val info = when (val app = selectedApp) {
|
selectedAppInfo = pm.getPackageInfo(packageName)
|
||||||
is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) }
|
|
||||||
is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) }
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedAppInfo = info
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOptionsFiltered(bundles: List<PatchBundleInfo.Scoped>) = options.filtered(bundles)
|
fun getOptionsFiltered(bundles: List<PatchBundleInfo.Scoped>) = options.filtered(bundles)
|
||||||
@@ -272,37 +251,50 @@ class SelectedAppInfoViewModel(
|
|||||||
val allowIncompatible = prefs.disablePatchVersionCompatCheck.get()
|
val allowIncompatible = prefs.disablePatchVersionCompatCheck.get()
|
||||||
val bundles = bundleInfoFlow.first()
|
val bundles = bundleInfoFlow.first()
|
||||||
return Patcher.ViewModelParams(
|
return Patcher.ViewModelParams(
|
||||||
selectedApp,
|
input.packageName,
|
||||||
getPatches(bundles, allowIncompatible),
|
resolvedVersion.first(),
|
||||||
|
resolvedSource.first(),
|
||||||
|
patchSelection.first(),
|
||||||
getOptionsFiltered(bundles)
|
getOptionsFiltered(bundles)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPatches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) =
|
init {
|
||||||
selectionState.patches(bundles, allowIncompatible)
|
invalidateSelectedAppInfo()
|
||||||
|
|
||||||
fun getCustomPatches(
|
input.localPath?.let { local ->
|
||||||
bundles: List<PatchBundleInfo.Scoped>,
|
viewModelScope.launch {
|
||||||
allowIncompatible: Boolean
|
val packageInfo = pm.getPackageInfo(File(local))
|
||||||
): PatchSelection? =
|
|
||||||
(selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible)
|
|
||||||
|
|
||||||
|
_selectedVersion.value = SelectedVersion.Specific(
|
||||||
|
packageInfo?.versionName ?: return@launch
|
||||||
|
)
|
||||||
|
_selectedSource.value = SelectedSource.Local(local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateConfiguration(
|
// Get the previous selection if customization is enabled.
|
||||||
selection: PatchSelection?,
|
viewModelScope.launch {
|
||||||
options: Options
|
if (prefs.disableSelectionWarning.get()) {
|
||||||
) = viewModelScope.launch {
|
val previous = selectionRepository.getSelection(packageName)
|
||||||
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
if (previous.patchCount == 0) return@launch
|
||||||
|
selectionFlow.value = SelectionState.Customized(previous)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val filteredOptions = options.filtered(bundleInfoFlow.first())
|
// Get installed app info
|
||||||
this@SelectedAppInfoViewModel.options = filteredOptions
|
viewModelScope.launch {
|
||||||
|
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
|
||||||
|
val installedAppDeferred =
|
||||||
|
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
|
||||||
|
|
||||||
if (!persistConfiguration) return@launch
|
// installedAppData =
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
// packageInfo.await()?.let {
|
||||||
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
// SelectedApp.Installed(
|
||||||
?: selectionRepository.resetSelectionForPackage(packageName)
|
// packageName,
|
||||||
|
// it.versionName!!
|
||||||
optionsRepository.saveOptions(packageName, filteredOptions)
|
// ) to installedAppDeferred.await()
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||||
|
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||||
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
|
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||||
|
import app.revanced.manager.ui.model.SelectedSource
|
||||||
|
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class SourceSelectorViewModel(
|
||||||
|
val input: SelectedAppInfo.SourceSelector.ViewModelParams
|
||||||
|
) : ViewModel(), KoinComponent {
|
||||||
|
private val app: Application = get()
|
||||||
|
private val downloadedAppRepository: DownloadedAppRepository = get()
|
||||||
|
private val pluginRepository: DownloaderPluginRepository = get()
|
||||||
|
private val installedAppRepository: InstalledAppRepository = get()
|
||||||
|
private val pm: PM = get()
|
||||||
|
|
||||||
|
var selectedSource by mutableStateOf(input.selectedSource)
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun selectSource(source: SelectedSource) {
|
||||||
|
selectedSource = source
|
||||||
|
}
|
||||||
|
|
||||||
|
var localApp by mutableStateOf<SourceOption?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
val downloadedApps = downloadedAppRepository.get(input.packageName)
|
||||||
|
.map { apps ->
|
||||||
|
apps.sortedByDescending { app -> app.version }
|
||||||
|
.map {
|
||||||
|
SourceOption(
|
||||||
|
source = SelectedSource.Downloaded(
|
||||||
|
path = downloadedAppRepository.getApkFileForApp(it).path,
|
||||||
|
version = it.version
|
||||||
|
),
|
||||||
|
title = it.version,
|
||||||
|
category = "Downloaded",
|
||||||
|
key = it.version,
|
||||||
|
disableReason = if (input.version != null && it.version != input.version) {
|
||||||
|
DisableReason.VERSION_NOT_MATCHING
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val plugins = pluginRepository.pluginStates.map { plugins ->
|
||||||
|
plugins.toList().sortedByDescending { it.second is DownloaderPluginState.Loaded }
|
||||||
|
.map {
|
||||||
|
val packageInfo = pm.getPackageInfo(it.first)
|
||||||
|
val label = packageInfo?.applicationInfo?.loadLabel(app.packageManager)
|
||||||
|
?.toString()
|
||||||
|
|
||||||
|
SourceOption(
|
||||||
|
source = SelectedSource.Plugin(it.first),
|
||||||
|
title = label ?: it.first,
|
||||||
|
category = "Plugin",
|
||||||
|
key = it.first,
|
||||||
|
disableReason = when (it.second) {
|
||||||
|
is DownloaderPluginState.Loaded -> null
|
||||||
|
is DownloaderPluginState.Untrusted -> DisableReason.NOT_TRUSTED
|
||||||
|
is DownloaderPluginState.Failed -> DisableReason.FAILED_TO_LOAD
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPackageInfo(packageName: String) = pm.getPackageInfo(packageName)
|
||||||
|
|
||||||
|
var installedSource by mutableStateOf<SourceOption?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val packageInfo = pm.getPackageInfo(input.packageName) ?: return@launch
|
||||||
|
|
||||||
|
val installedApp = installedAppRepository.get(input.packageName)
|
||||||
|
|
||||||
|
installedSource = SourceOption(
|
||||||
|
source = SelectedSource.Installed,
|
||||||
|
title = packageInfo.versionName.toString(),
|
||||||
|
category = "Installed",
|
||||||
|
key = input.packageName,
|
||||||
|
disableReason = when {
|
||||||
|
installedApp != null -> DisableReason.ALREADY_PATCHED
|
||||||
|
input.version != null && packageInfo.versionName != input.version -> DisableReason.VERSION_NOT_MATCHING
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
input.localPath?.let { local ->
|
||||||
|
viewModelScope.launch {
|
||||||
|
val packageInfo = pm.getPackageInfo(File(local))
|
||||||
|
?: return@launch
|
||||||
|
|
||||||
|
localApp = SourceOption(
|
||||||
|
source = SelectedSource.Local(local),
|
||||||
|
title = packageInfo.versionName.toString(),
|
||||||
|
category = "Local",
|
||||||
|
key = "local",
|
||||||
|
disableReason = if (input.version != null && packageInfo.versionName != input.version) {
|
||||||
|
DisableReason.VERSION_NOT_MATCHING
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DisableReason(val message: String) {
|
||||||
|
VERSION_NOT_MATCHING("Does not match the selected version"),
|
||||||
|
ALREADY_PATCHED("Already patched"),
|
||||||
|
NOT_TRUSTED("Not trusted"),
|
||||||
|
FAILED_TO_LOAD("Failed to load"),
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SourceOption(
|
||||||
|
val source: SelectedSource,
|
||||||
|
val title: String,
|
||||||
|
val category: String? = null,
|
||||||
|
val key: String,
|
||||||
|
val disableReason: DisableReason? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ class UpdateViewModel(
|
|||||||
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
|
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
|
||||||
val release = releaseInfo!!
|
val release = releaseInfo!!
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (!networkInfo.isSafe(false) && !ignoreInternetCheck) {
|
if (!networkInfo.isSafe() && !ignoreInternetCheck) {
|
||||||
showInternetCheckDialog = true
|
showInternetCheckDialog = true
|
||||||
} else {
|
} else {
|
||||||
state = State.DOWNLOADING
|
state = State.DOWNLOADING
|
||||||
@@ -90,10 +90,8 @@ class UpdateViewModel(
|
|||||||
http.download(location) {
|
http.download(location) {
|
||||||
url(release.downloadUrl)
|
url(release.downloadUrl)
|
||||||
onDownload { bytesSentTotal, contentLength ->
|
onDownload { bytesSentTotal, contentLength ->
|
||||||
withContext(Dispatchers.Main) {
|
downloadedSize = bytesSentTotal
|
||||||
downloadedSize = bytesSentTotal
|
totalSize = contentLength
|
||||||
contentLength?.let { totalSize = it }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
installUpdate()
|
installUpdate()
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||||
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.ui.model.SelectedVersion
|
||||||
|
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import app.revanced.manager.util.patchCount
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class VersionSelectorViewModel(
|
||||||
|
val input: SelectedAppInfo.VersionSelector.ViewModelParams
|
||||||
|
) : ViewModel(), KoinComponent {
|
||||||
|
private val patchBundleRepository: PatchBundleRepository = get()
|
||||||
|
private val downloadedAppsRepository: DownloadedAppRepository = get()
|
||||||
|
private val installedAppRepository: InstalledAppRepository = get()
|
||||||
|
private val pm: PM = get()
|
||||||
|
|
||||||
|
val patchCount = input.patchSelection.patchCount
|
||||||
|
|
||||||
|
val downloadedVersions = downloadedAppsRepository.get(input.packageName)
|
||||||
|
.map { apps ->
|
||||||
|
apps.map { it.version }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _localVersion = MutableStateFlow<String?>(null)
|
||||||
|
val localVersion: StateFlow<String?> = _localVersion
|
||||||
|
|
||||||
|
val availableVersions = combine(
|
||||||
|
patchBundleRepository.suggestedVersions(input.packageName, input.patchSelection),
|
||||||
|
_localVersion,
|
||||||
|
) { versions, local ->
|
||||||
|
versions.orEmpty()
|
||||||
|
.let { versions ->
|
||||||
|
local?.let {
|
||||||
|
versions.toMutableMap().also { it.putIfAbsent(local, 0) }
|
||||||
|
} ?: versions
|
||||||
|
}
|
||||||
|
.map { (key, value) -> SelectedVersion.Specific(key) to patchCount - value }
|
||||||
|
.sortedWith(
|
||||||
|
compareBy<Pair<SelectedVersion.Specific, Int>>{ it.second }
|
||||||
|
.thenByDescending { it.first.version }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var installedAppVersion by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val currentApp = pm.getPackageInfo(input.packageName)
|
||||||
|
val patchedApp = installedAppRepository.get(input.packageName)
|
||||||
|
|
||||||
|
// Skip if installed app is patched
|
||||||
|
if (patchedApp?.currentPackageName == input.packageName) return@launch
|
||||||
|
|
||||||
|
installedAppVersion = currentApp?.versionName
|
||||||
|
}
|
||||||
|
input.localPath?.let { local ->
|
||||||
|
viewModelScope.launch {
|
||||||
|
val packageInfo = pm.getPackageInfo(File(local))
|
||||||
|
_localVersion.value = packageInfo?.versionName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedVersion by mutableStateOf(input.selectedVersion)
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun selectVersion(version: SelectedVersion) {
|
||||||
|
selectedVersion = version
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package app.revanced.manager.util
|
|
||||||
|
|
||||||
import android.app.LocaleConfig
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.LocaleList
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.core.os.LocaleListCompat
|
|
||||||
import app.revanced.manager.BuildConfig
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
object SupportedLocales {
|
|
||||||
fun getSupportedLocales(context: Context): List<Locale> {
|
|
||||||
var result: List<Locale>? = null
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) result = runCatching {
|
|
||||||
LocaleConfig(context).supportedLocales?.toList()
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
return result ?: generated
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentLocale(): Locale? =
|
|
||||||
AppCompatDelegate.getApplicationLocales().takeIf { !it.isEmpty }?.get(0)
|
|
||||||
|
|
||||||
fun setLocale(locale: Locale?) = AppCompatDelegate.setApplicationLocales(
|
|
||||||
locale?.let { LocaleListCompat.create(it) } ?: LocaleListCompat.getEmptyLocaleList()
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getDisplayName(locale: Locale) =
|
|
||||||
locale.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) }
|
|
||||||
|
|
||||||
private fun LocaleList.toList() = (0 until size()).map { get(it) }
|
|
||||||
|
|
||||||
private val generated by lazy {
|
|
||||||
listOf(
|
|
||||||
Locale.ENGLISH,
|
|
||||||
*BuildConfig.SUPPORTED_LOCALES.map(Locale::forLanguageTag).toTypedArray()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,10 +15,12 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -41,6 +43,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.format.MonthNames
|
import kotlinx.datetime.format.MonthNames
|
||||||
@@ -48,15 +51,16 @@ import kotlinx.datetime.format.char
|
|||||||
import kotlinx.datetime.toInstant
|
import kotlinx.datetime.toInstant
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.properties.PropertyDelegateProvider
|
import kotlin.properties.PropertyDelegateProvider
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
import kotlin.time.Clock
|
|
||||||
|
|
||||||
typealias PatchSelection = Map<Int, Set<String>>
|
typealias PatchSelection = Map<Int, Set<String>>
|
||||||
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
|
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
|
||||||
|
|
||||||
|
val PatchSelection.patchCount
|
||||||
|
get() = this.values.sumOf { it.size }
|
||||||
|
|
||||||
val Context.isDebuggable get() = 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
|
val Context.isDebuggable get() = 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
|
||||||
|
|
||||||
fun Context.openUrl(url: String) {
|
fun Context.openUrl(url: String) {
|
||||||
@@ -168,7 +172,7 @@ fun LocalDateTime.relativeTime(context: Context): String {
|
|||||||
else -> LocalDateTime.Format {
|
else -> LocalDateTime.Format {
|
||||||
monthName(MonthNames.ENGLISH_ABBREVIATED)
|
monthName(MonthNames.ENGLISH_ABBREVIATED)
|
||||||
char(' ')
|
char(' ')
|
||||||
day()
|
dayOfMonth()
|
||||||
if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) {
|
if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) {
|
||||||
chars(", ")
|
chars(", ")
|
||||||
year()
|
year()
|
||||||
@@ -195,12 +199,7 @@ val transparentListItemColors
|
|||||||
.also { transparentListItemColorsCached = it }
|
.also { transparentListItemColorsCached = it }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> EventEffect(
|
fun <T> EventEffect(flow: Flow<T>, vararg keys: Any?, state: Lifecycle.State = Lifecycle.State.STARTED, block: suspend (T) -> Unit) {
|
||||||
flow: Flow<T>,
|
|
||||||
vararg keys: Any?,
|
|
||||||
state: Lifecycle.State = Lifecycle.State.STARTED,
|
|
||||||
block: suspend (T) -> Unit
|
|
||||||
) {
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val currentBlock by rememberUpdatedState(block)
|
val currentBlock by rememberUpdatedState(block)
|
||||||
|
|
||||||
@@ -216,36 +215,40 @@ fun <T> EventEffect(
|
|||||||
const val isScrollingUpSensitivity = 10
|
const val isScrollingUpSensitivity = 10
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LazyListState.isScrollingUp() = produceState(true, this) {
|
fun LazyListState.isScrollingUp(): State<Boolean> {
|
||||||
var previousIndex = firstVisibleItemIndex
|
return remember(this) {
|
||||||
var previousScrollOffset = firstVisibleItemScrollOffset
|
var previousIndex by mutableIntStateOf(firstVisibleItemIndex)
|
||||||
|
var previousScrollOffset by mutableIntStateOf(firstVisibleItemScrollOffset)
|
||||||
|
|
||||||
snapshotFlow {
|
derivedStateOf {
|
||||||
firstVisibleItemIndex to firstVisibleItemScrollOffset
|
val indexChanged = previousIndex != firstVisibleItemIndex
|
||||||
}.collect { (index, scrollOffset) ->
|
val offsetChanged =
|
||||||
val indexChanged = previousIndex != index
|
kotlin.math.abs(previousScrollOffset - firstVisibleItemScrollOffset) > isScrollingUpSensitivity
|
||||||
val offsetChanged = abs(previousScrollOffset - scrollOffset) > isScrollingUpSensitivity
|
|
||||||
|
|
||||||
value = when {
|
if (indexChanged) {
|
||||||
indexChanged -> previousIndex > index
|
previousIndex > firstVisibleItemIndex
|
||||||
offsetChanged -> previousScrollOffset > scrollOffset
|
} else if (offsetChanged) {
|
||||||
else -> value
|
previousScrollOffset > firstVisibleItemScrollOffset
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}.also {
|
||||||
|
previousIndex = firstVisibleItemIndex
|
||||||
|
previousScrollOffset = firstVisibleItemScrollOffset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
previousIndex = index
|
|
||||||
previousScrollOffset = scrollOffset
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: support sensitivity
|
||||||
@Composable
|
@Composable
|
||||||
fun ScrollState.isScrollingUp() = produceState(true, this) {
|
fun ScrollState.isScrollingUp(): State<Boolean> {
|
||||||
var previousScrollOffset = this@isScrollingUp.value
|
return remember(this) {
|
||||||
|
var previousScrollOffset by mutableIntStateOf(value)
|
||||||
snapshotFlow { this@isScrollingUp.value }.collect { scrollOffset ->
|
derivedStateOf {
|
||||||
if (abs(previousScrollOffset - scrollOffset) > isScrollingUpSensitivity) {
|
(previousScrollOffset >= value).also {
|
||||||
value = previousScrollOffset >= scrollOffset
|
previousScrollOffset = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
previousScrollOffset = scrollOffset
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Second \"item\" text"</string>
|
|||||||
-->
|
-->
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">ReVanced Manager</string>
|
<string name="app_name">ReVanced Manager</string>
|
||||||
<string name="patcher">Patcher test</string>
|
<string name="patcher">Patcher</string>
|
||||||
<string name="patches">Patches</string>
|
<string name="patches">Patches</string>
|
||||||
<string name="cli">CLI</string>
|
<string name="cli">CLI</string>
|
||||||
<string name="manager">Manager</string>
|
<string name="manager">Manager</string>
|
||||||
@@ -61,14 +61,15 @@ Second \"item\" text"</string>
|
|||||||
<string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not match the suggested version</string>
|
<string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not match the suggested version</string>
|
||||||
|
|
||||||
<string name="patch_item_description">Start patching the application</string>
|
<string name="patch_item_description">Start patching the application</string>
|
||||||
<string name="patch_selector_item">Select patches</string>
|
<string name="patch_selector_item">Patches</string>
|
||||||
<string name="patch_selector_item_description">%d patches selected</string>
|
<string name="patch_selector_item_description">%d selected</string>
|
||||||
<string name="no_patches_selected">No patches selected</string>
|
<string name="no_patches_selected">No patches selected</string>
|
||||||
|
<string name="version_selector_item">Version</string>
|
||||||
|
|
||||||
<string name="network_unavailable_warning">Your device is not connected to the internet. Downloading will fail later.</string>
|
<string name="network_unavailable_warning">Your device is not connected to the internet. Downloading will fail later.</string>
|
||||||
<string name="network_metered_warning">You are currently on a metered connection. Data charges from your service provider may apply.</string>
|
<string name="network_metered_warning">You are currently on a metered connection. Data charges from your service provider may apply.</string>
|
||||||
|
|
||||||
<string name="apk_source_selector_item">Select APK source</string>
|
<string name="apk_source_selector_item">APK source</string>
|
||||||
<string name="apk_source_auto">Using all APK downloaders</string>
|
<string name="apk_source_auto">Using all APK downloaders</string>
|
||||||
<string name="apk_source_downloader">Using %s</string>
|
<string name="apk_source_downloader">Using %s</string>
|
||||||
<string name="apk_source_installed">Using installed APK</string>
|
<string name="apk_source_installed">Using installed APK</string>
|
||||||
@@ -83,7 +84,7 @@ Second \"item\" text"</string>
|
|||||||
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
||||||
|
|
||||||
<string name="general">General</string>
|
<string name="general">General</string>
|
||||||
<string name="general_description">Language, theme, dynamic color</string>
|
<string name="general_description">Theme, dynamic color</string>
|
||||||
<string name="updates">Updates</string>
|
<string name="updates">Updates</string>
|
||||||
<string name="updates_description">Check for updates and view changelogs</string>
|
<string name="updates_description">Check for updates and view changelogs</string>
|
||||||
<string name="downloads">Downloads</string>
|
<string name="downloads">Downloads</string>
|
||||||
@@ -104,9 +105,6 @@ Second \"item\" text"</string>
|
|||||||
<string name="pure_black_theme_description">Use pure black backgrounds for dark theme</string>
|
<string name="pure_black_theme_description">Use pure black backgrounds for dark theme</string>
|
||||||
<string name="theme">Theme</string>
|
<string name="theme">Theme</string>
|
||||||
<string name="theme_description">Choose between light or dark theme</string>
|
<string name="theme_description">Choose between light or dark theme</string>
|
||||||
<string name="language">Language</string>
|
|
||||||
<string name="language_description">Choose the app display language</string>
|
|
||||||
<string name="language_system_default">System default</string>
|
|
||||||
<string name="safeguards">Safeguards</string>
|
<string name="safeguards">Safeguards</string>
|
||||||
<string name="patch_compat_check">Disable version compatibility check</string>
|
<string name="patch_compat_check">Disable version compatibility check</string>
|
||||||
<string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string>
|
<string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string>
|
||||||
@@ -205,7 +203,7 @@ You will not be able to update the previously installed apps from this source."<
|
|||||||
<string name="share">Share</string>
|
<string name="share">Share</string>
|
||||||
<string name="patch">Patch</string>
|
<string name="patch">Patch</string>
|
||||||
<string name="select_from_storage">Select from storage</string>
|
<string name="select_from_storage">Select from storage</string>
|
||||||
<string name="select_from_storage_description">Select an APK file from storage using file picker</string>
|
<string name="select_from_storage_description">Select an APK file from storage</string>
|
||||||
<string name="suggested_version_info">Suggested version: %s</string>
|
<string name="suggested_version_info">Suggested version: %s</string>
|
||||||
<string name="type_anything">Type anything to continue</string>
|
<string name="type_anything">Type anything to continue</string>
|
||||||
<string name="search">Search patches…</string>
|
<string name="search">Search patches…</string>
|
||||||
@@ -220,10 +218,6 @@ You will not be able to update the previously installed apps from this source."<
|
|||||||
<string name="light">Light</string>
|
<string name="light">Light</string>
|
||||||
<string name="dark">Dark</string>
|
<string name="dark">Dark</string>
|
||||||
<string name="appearance">Appearance</string>
|
<string name="appearance">Appearance</string>
|
||||||
<string name="networking">Networking</string>
|
|
||||||
<string name="allow_metered_networks">Allow metered networks</string>
|
|
||||||
<string name="allow_metered_networks_description">Permits automatic updates on metered networks.
|
|
||||||
The application might still warn about metered networks for manual operations.</string>
|
|
||||||
<string name="downloaded_apps">Downloaded apps</string>
|
<string name="downloaded_apps">Downloaded apps</string>
|
||||||
<string name="process_runtime">Run Patcher in another process (experimental)</string>
|
<string name="process_runtime">Run Patcher in another process (experimental)</string>
|
||||||
<string name="process_runtime_description">This is faster and allows Patcher to use more memory</string>
|
<string name="process_runtime_description">This is faster and allows Patcher to use more memory</string>
|
||||||
|
|||||||
@@ -6,6 +6,5 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.serialization) apply false
|
alias(libs.plugins.kotlin.serialization) apply false
|
||||||
alias(libs.plugins.kotlin.parcelize) apply false
|
alias(libs.plugins.kotlin.parcelize) apply false
|
||||||
alias(libs.plugins.about.libraries) apply false
|
alias(libs.plugins.about.libraries) apply false
|
||||||
alias(libs.plugins.about.libraries.android) apply false
|
|
||||||
alias(libs.plugins.compose.compiler) apply false
|
alias(libs.plugins.compose.compiler) apply false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
project_id_env: "CROWDIN_PROJECT_ID"
|
|
||||||
api_token_env: "CROWDIN_PERSONAL_TOKEN"
|
|
||||||
|
|
||||||
preserve_hierarchy: true
|
|
||||||
files:
|
|
||||||
- source: app/src/main/res/values/strings.xml
|
|
||||||
dest: manager.xml
|
|
||||||
translation: app/src/main/res/values-%android_code%/strings.xml
|
|
||||||
skip_untranslated_strings: true
|
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
[versions]
|
[versions]
|
||||||
ktx = "1.17.0"
|
ktx = "1.16.0"
|
||||||
material3 = "1.4.0"
|
material3 = "1.3.2"
|
||||||
ui-tooling = "1.10.0"
|
ui-tooling = "1.8.1"
|
||||||
viewmodel-lifecycle = "2.10.0"
|
viewmodel-lifecycle = "2.9.0"
|
||||||
splash-screen = "1.2.0"
|
splash-screen = "1.0.1"
|
||||||
activity = "1.12.2"
|
activity = "1.10.1"
|
||||||
appcompat = "1.7.1"
|
appcompat = "1.7.0"
|
||||||
preferences-datastore = "1.2.0"
|
preferences-datastore = "1.1.2"
|
||||||
work-runtime = "2.11.0"
|
work-runtime = "2.10.1"
|
||||||
compose-bom = "2025.12.01"
|
compose-bom = "2025.05.00"
|
||||||
navigation = "2.9.6"
|
navigation = "2.8.6"
|
||||||
accompanist = "0.37.3"
|
accompanist = "0.37.0"
|
||||||
placeholder = "1.0.12"
|
placeholder = "1.1.2"
|
||||||
reorderable = "3.0.0"
|
reorderable = "2.4.3"
|
||||||
serialization = "1.9.0"
|
serialization = "1.8.0"
|
||||||
collection = "0.4.0"
|
collection = "0.3.8"
|
||||||
datetime = "0.7.1"
|
datetime = "0.6.1"
|
||||||
room-version = "2.8.4"
|
room-version = "2.7.1"
|
||||||
revanced-patcher = "21.0.0"
|
revanced-patcher = "21.0.0"
|
||||||
revanced-library = "3.0.2"
|
revanced-library = "3.0.2"
|
||||||
koin = "4.1.1"
|
koin = "3.5.3"
|
||||||
ktor = "3.3.3"
|
ktor = "2.3.9"
|
||||||
markdown-renderer = "0.39.0"
|
markdown-renderer = "0.30.0"
|
||||||
fading-edges = "1.0.4"
|
fading-edges = "1.0.4"
|
||||||
kotlin = "2.3.0"
|
kotlin = "2.1.10"
|
||||||
android-gradle-plugin = "8.13.2"
|
android-gradle-plugin = "8.9.1"
|
||||||
dev-tools-gradle-plugin = "2.3.4"
|
dev-tools-gradle-plugin = "2.1.10-1.0.29"
|
||||||
about-libraries = "13.2.1"
|
about-libraries-gradle-plugin = "12.1.2"
|
||||||
coil = "2.7.0"
|
coil = "2.7.0"
|
||||||
app-icon-loader-coil = "1.5.0"
|
app-icon-loader-coil = "1.5.0"
|
||||||
libsu = "6.0.0"
|
libsu = "6.0.0"
|
||||||
@@ -34,10 +34,10 @@ scrollbars = "1.0.4"
|
|||||||
enumutil = "1.1.1"
|
enumutil = "1.1.1"
|
||||||
compose-icons = "1.2.4"
|
compose-icons = "1.2.4"
|
||||||
kotlin-process = "1.5.1"
|
kotlin-process = "1.5.1"
|
||||||
hidden-api-stub = "4.4.0"
|
hidden-api-stub = "4.3.3"
|
||||||
binary-compatibility-validator = "0.18.1"
|
binary-compatibility-validator = "0.17.0"
|
||||||
semver-parser = "3.0.0"
|
semver-parser = "3.0.0"
|
||||||
ackpine = "0.19.1"
|
ackpine = "0.18.5"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# AndroidX Core
|
# AndroidX Core
|
||||||
@@ -68,7 +68,7 @@ coil-appiconloader = { group = "me.zhanghai.android.appiconloader", name = "appi
|
|||||||
accompanist-drawablepainter = { group = "com.google.accompanist", name = "accompanist-drawablepainter", version.ref = "accompanist" }
|
accompanist-drawablepainter = { group = "com.google.accompanist", name = "accompanist-drawablepainter", version.ref = "accompanist" }
|
||||||
|
|
||||||
# Placeholder
|
# Placeholder
|
||||||
placeholder-material3 = { group = "com.eygraber", name = "compose-placeholder-material3", version.ref = "placeholder" }
|
placeholder-material3 = { group = "io.github.fornewid", name = "placeholder-material3", version.ref = "placeholder"}
|
||||||
|
|
||||||
# Kotlinx
|
# Kotlinx
|
||||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
|
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
|
||||||
@@ -91,8 +91,7 @@ koin-compose-navigation = { group = "io.insert-koin", name = "koin-androidx-comp
|
|||||||
koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" }
|
koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" }
|
||||||
|
|
||||||
# About Libraries
|
# About Libraries
|
||||||
about-libraries-core = { group = "com.mikepenz", name = "aboutlibraries-compose-core", version.ref = "about-libraries" }
|
about-libraries = { group = "com.mikepenz", name = "aboutlibraries-compose", version.ref = "about-libraries-gradle-plugin" }
|
||||||
about-libraries-m3 = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "about-libraries" }
|
|
||||||
|
|
||||||
# Ktor
|
# Ktor
|
||||||
ktor-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
|
ktor-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
|
||||||
@@ -147,6 +146,5 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
|
|||||||
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
||||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" }
|
devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" }
|
||||||
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries" }
|
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" }
|
||||||
about-libraries-android = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "about-libraries" }
|
|
||||||
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
|
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
|
||||||
|
|||||||
Reference in New Issue
Block a user