mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-20 09:43:58 +00:00
Compare commits
10 Commits
feat/impro
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7615453eec | ||
|
|
4c0b6b02e9 | ||
|
|
fe84b22b6f | ||
|
|
1b21f5d4ab | ||
|
|
72b1db9a2f | ||
|
|
2805ac6540 | ||
|
|
b16931ca79 | ||
|
|
dfeca09d00 | ||
|
|
44c06e2197 | ||
|
|
df31b39cc8 |
43
.github/workflows/pull_strings.yml
vendored
Normal file
43
.github/workflows/pull_strings.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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
Normal file
26
.github/workflows/push_strings.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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 }}
|
||||||
@@ -73,7 +73,7 @@ ReVanced Manager is an application that uses [ReVanced Patcher](https://github.c
|
|||||||
|
|
||||||
Some of the features ReVanced Manager provides are:
|
Some of the features ReVanced Manager provides are:
|
||||||
|
|
||||||
- ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader system
|
- ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader plugin system
|
||||||
- 💉 **Patch**: Select and apply patches to any Android app
|
- 💉 **Patch**: Select and apply patches to any Android app
|
||||||
- 🛠️ **Customize**: Manage patches, apps, signing, themes, updates, and many more settings
|
- 🛠️ **Customize**: Manage patches, apps, signing, themes, updates, and many more settings
|
||||||
|
|
||||||
|
|||||||
106
api/api/api.api
106
api/api/api.api
@@ -1,19 +1,19 @@
|
|||||||
public abstract interface class app/revanced/manager/downloader/BaseDownloadScope : app/revanced/manager/downloader/Scope {
|
public abstract interface class app/revanced/manager/plugin/downloader/BaseDownloadScope : app/revanced/manager/plugin/downloader/Scope {
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/ConstantsKt {
|
public final class app/revanced/manager/plugin/downloader/ConstantsKt {
|
||||||
public static final field DOWNLOADER_HOST_PERMISSION Ljava/lang/String;
|
public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/DownloadUrl : android/os/Parcelable {
|
public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable {
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
public static final field CREATOR Landroid/os/Parcelable$Creator;
|
public static final field CREATOR Landroid/os/Parcelable$Creator;
|
||||||
public fun <init> (Ljava/lang/String;Ljava/util/Map;)V
|
public fun <init> (Ljava/lang/String;Ljava/util/Map;)V
|
||||||
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
public final fun component1 ()Ljava/lang/String;
|
public final fun component1 ()Ljava/lang/String;
|
||||||
public final fun component2 ()Ljava/util/Map;
|
public final fun component2 ()Ljava/util/Map;
|
||||||
public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/downloader/DownloadUrl;
|
public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||||
public static synthetic fun copy$default (Lapp/revanced/manager/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/downloader/DownloadUrl;
|
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||||
public final fun describeContents ()I
|
public final fun describeContents ()I
|
||||||
public fun equals (Ljava/lang/Object;)Z
|
public fun equals (Ljava/lang/Object;)Z
|
||||||
public final fun getHeaders ()Ljava/util/Map;
|
public final fun getHeaders ()Ljava/util/Map;
|
||||||
@@ -24,61 +24,50 @@ public final class app/revanced/manager/downloader/DownloadUrl : android/os/Parc
|
|||||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
|
public final class app/revanced/manager/plugin/downloader/Downloader {
|
||||||
public fun <init> ()V
|
|
||||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/downloader/DownloadUrl;
|
|
||||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
|
||||||
public final fun newArray (I)[Lapp/revanced/manager/downloader/DownloadUrl;
|
|
||||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/Downloader {
|
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/DownloaderBuilder {
|
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract interface annotation class app/revanced/manager/downloader/DownloaderHostApi : java/lang/annotation/Annotation {
|
public final class app/revanced/manager/plugin/downloader/DownloaderKt {
|
||||||
|
public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/DownloaderKt {
|
public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope {
|
||||||
public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/downloader/DownloaderBuilder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/DownloaderScope : app/revanced/manager/downloader/Scope {
|
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
public final fun download (Lkotlin/jvm/functions/Function3;)V
|
public final fun download (Lkotlin/jvm/functions/Function3;)V
|
||||||
public final fun get (Lkotlin/jvm/functions/Function4;)V
|
public final fun get (Lkotlin/jvm/functions/Function4;)V
|
||||||
public fun getDownloaderPackageName ()Ljava/lang/String;
|
|
||||||
public fun getHostPackageName ()Ljava/lang/String;
|
public fun getHostPackageName ()Ljava/lang/String;
|
||||||
|
public fun getPluginPackageName ()Ljava/lang/String;
|
||||||
public final fun useService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
public final fun useService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/ExtensionsKt {
|
public final class app/revanced/manager/plugin/downloader/ExtensionsKt {
|
||||||
public static final fun download (Lapp/revanced/manager/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V
|
public static final fun download (Lapp/revanced/manager/plugin/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract interface class app/revanced/manager/downloader/GetScope : app/revanced/manager/downloader/Scope {
|
public abstract interface class app/revanced/manager/plugin/downloader/GetScope : app/revanced/manager/plugin/downloader/Scope {
|
||||||
public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract interface class app/revanced/manager/downloader/InputDownloadScope : app/revanced/manager/downloader/BaseDownloadScope {
|
public abstract interface class app/revanced/manager/plugin/downloader/InputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract interface class app/revanced/manager/downloader/OutputDownloadScope : app/revanced/manager/downloader/BaseDownloadScope {
|
public abstract interface class app/revanced/manager/plugin/downloader/OutputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
|
||||||
public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/Package : android/os/Parcelable {
|
public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable {
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
public static final field CREATOR Landroid/os/Parcelable$Creator;
|
public static final field CREATOR Landroid/os/Parcelable$Creator;
|
||||||
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
|
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
|
||||||
public final fun component1 ()Ljava/lang/String;
|
public final fun component1 ()Ljava/lang/String;
|
||||||
public final fun component2 ()Ljava/lang/String;
|
public final fun component2 ()Ljava/lang/String;
|
||||||
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/downloader/Package;
|
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package;
|
||||||
public static synthetic fun copy$default (Lapp/revanced/manager/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/downloader/Package;
|
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package;
|
||||||
public final fun describeContents ()I
|
public final fun describeContents ()I
|
||||||
public fun equals (Ljava/lang/Object;)Z
|
public fun equals (Ljava/lang/Object;)Z
|
||||||
public final fun getName ()Ljava/lang/String;
|
public final fun getName ()Ljava/lang/String;
|
||||||
@@ -88,95 +77,82 @@ public final class app/revanced/manager/downloader/Package : android/os/Parcelab
|
|||||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/Package$Creator : android/os/Parcelable$Creator {
|
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
|
||||||
public fun <init> ()V
|
|
||||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/downloader/Package;
|
|
||||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
|
||||||
public final fun newArray (I)[Lapp/revanced/manager/downloader/Package;
|
|
||||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract interface class app/revanced/manager/downloader/Scope {
|
public abstract interface class app/revanced/manager/plugin/downloader/Scope {
|
||||||
public abstract fun getDownloaderPackageName ()Ljava/lang/String;
|
|
||||||
public abstract fun getHostPackageName ()Ljava/lang/String;
|
public abstract fun getHostPackageName ()Ljava/lang/String;
|
||||||
|
public abstract fun getPluginPackageName ()Ljava/lang/String;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class app/revanced/manager/downloader/UserInteractionException : java/lang/Exception {
|
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class app/revanced/manager/downloader/UserInteractionException$Activity : app/revanced/manager/downloader/UserInteractionException {
|
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException {
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/downloader/UserInteractionException$Activity {
|
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/downloader/UserInteractionException$Activity {
|
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
public final fun getIntent ()Landroid/content/Intent;
|
public final fun getIntent ()Landroid/content/Intent;
|
||||||
public final fun getResultCode ()I
|
public final fun getResultCode ()I
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/UserInteractionException$RequestDenied : app/revanced/manager/downloader/UserInteractionException {
|
public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/webview/APIKt {
|
public final class app/revanced/manager/plugin/downloader/webview/APIKt {
|
||||||
public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/downloader/DownloaderBuilder;
|
public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
|
||||||
public static final fun runWebView (Lapp/revanced/manager/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
public static final fun runWebView (Lapp/revanced/manager/plugin/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class app/revanced/manager/downloader/webview/IWebView$Default : app/revanced/manager/downloader/webview/IWebView {
|
public class app/revanced/manager/plugin/downloader/webview/IWebView$Default : app/revanced/manager/plugin/downloader/webview/IWebView {
|
||||||
public fun <init> ()V
|
public fun <init> ()V
|
||||||
public fun asBinder ()Landroid/os/IBinder;
|
public fun asBinder ()Landroid/os/IBinder;
|
||||||
public fun finish ()V
|
public fun finish ()V
|
||||||
public fun load (Ljava/lang/String;)V
|
public fun load (Ljava/lang/String;)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class app/revanced/manager/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/downloader/webview/IWebView {
|
public abstract class app/revanced/manager/plugin/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebView {
|
||||||
public fun <init> ()V
|
public fun <init> ()V
|
||||||
public fun asBinder ()Landroid/os/IBinder;
|
public fun asBinder ()Landroid/os/IBinder;
|
||||||
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/downloader/webview/IWebView;
|
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebView;
|
||||||
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
||||||
}
|
}
|
||||||
|
|
||||||
public class app/revanced/manager/downloader/webview/IWebViewEvents$Default : app/revanced/manager/downloader/webview/IWebViewEvents {
|
public class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Default : app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
|
||||||
public fun <init> ()V
|
public fun <init> ()V
|
||||||
public fun asBinder ()Landroid/os/IBinder;
|
public fun asBinder ()Landroid/os/IBinder;
|
||||||
public fun download (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
|
public fun download (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
|
||||||
public fun pageLoad (Ljava/lang/String;)V
|
public fun pageLoad (Ljava/lang/String;)V
|
||||||
public fun ready (Lapp/revanced/manager/downloader/webview/IWebView;)V
|
public fun ready (Lapp/revanced/manager/plugin/downloader/webview/IWebView;)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class app/revanced/manager/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/downloader/webview/IWebViewEvents {
|
public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
|
||||||
public fun <init> ()V
|
public fun <init> ()V
|
||||||
public fun asBinder ()Landroid/os/IBinder;
|
public fun asBinder ()Landroid/os/IBinder;
|
||||||
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/downloader/webview/IWebViewEvents;
|
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebViewEvents;
|
||||||
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/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
|
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
|
||||||
public fun <init> ()V
|
|
||||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/downloader/webview/WebViewActivity$Parameters;
|
|
||||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
|
||||||
public final fun newArray (I)[Lapp/revanced/manager/downloader/webview/WebViewActivity$Parameters;
|
|
||||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract interface class app/revanced/manager/downloader/webview/WebViewCallbackScope : app/revanced/manager/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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/downloader/webview/WebViewScope : app/revanced/manager/downloader/Scope {
|
public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope {
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
public final fun download (Lkotlin/jvm/functions/Function5;)V
|
public final fun download (Lkotlin/jvm/functions/Function5;)V
|
||||||
public fun getDownloaderPackageName ()Ljava/lang/String;
|
|
||||||
public fun getHostPackageName ()Ljava/lang/String;
|
public fun getHostPackageName ()Ljava/lang/String;
|
||||||
|
public fun getPluginPackageName ()Ljava/lang/String;
|
||||||
public final fun pageLoad (Lkotlin/jvm/functions/Function3;)V
|
public final fun pageLoad (Lkotlin/jvm/functions/Function3;)V
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
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)
|
||||||
@@ -17,9 +19,16 @@ dependencies {
|
|||||||
implementation(libs.appcompat)
|
implementation(libs.appcompat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.manager.downloader"
|
namespace = "app.revanced.manager.plugin.downloader"
|
||||||
compileSdk = 35
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
@@ -42,17 +51,13 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
aidl = true
|
aidl = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apiValidation {
|
apiValidation {
|
||||||
nonPublicMarkers += "app.revanced.manager.downloader.DownloaderHostApi"
|
nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi"
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
// IWebView.aidl
|
|
||||||
package app.revanced.manager.downloader.webview;
|
|
||||||
|
|
||||||
@JavaPassthrough(annotation="@app.revanced.manager.downloader.DownloaderHostApi")
|
|
||||||
oneway interface IWebView {
|
|
||||||
void load(String url);
|
|
||||||
void finish();
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// IWebViewEvents.aidl
|
|
||||||
package app.revanced.manager.downloader.webview;
|
|
||||||
|
|
||||||
import app.revanced.manager.downloader.webview.IWebView;
|
|
||||||
|
|
||||||
@JavaPassthrough(annotation="@app.revanced.manager.downloader.DownloaderHostApi")
|
|
||||||
oneway interface IWebViewEvents {
|
|
||||||
void ready(IWebView iface);
|
|
||||||
void pageLoad(String url);
|
|
||||||
void download(String url, String mimetype, String userAgent);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// IWebView.aidl
|
||||||
|
package app.revanced.manager.plugin.downloader.webview;
|
||||||
|
|
||||||
|
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
|
||||||
|
oneway interface IWebView {
|
||||||
|
void load(String url);
|
||||||
|
void finish();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// IWebViewEvents.aidl
|
||||||
|
package app.revanced.manager.plugin.downloader.webview;
|
||||||
|
|
||||||
|
import app.revanced.manager.plugin.downloader.webview.IWebView;
|
||||||
|
|
||||||
|
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
|
||||||
|
oneway interface IWebViewEvents {
|
||||||
|
void ready(IWebView iface);
|
||||||
|
void pageLoad(String url);
|
||||||
|
void download(String url, String mimetype, String userAgent);
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package app.revanced.manager.downloader
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The permission ID of the special downloader host permission. Only ReVanced Manager will have this permission.
|
|
||||||
* Downloader UI activities and internal services can be protected using this permission.
|
|
||||||
*/
|
|
||||||
const val DOWNLOADER_HOST_PERMISSION = "app.revanced.manager.permission.DOWNLOADER_HOST"
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package app.revanced.manager.plugin.downloader
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The permission ID of the special plugin host permission. Only ReVanced Manager will have this permission.
|
||||||
|
* Plugin UI activities and internal services can be protected using this permission.
|
||||||
|
*/
|
||||||
|
const val PLUGIN_HOST_PERMISSION = "app.revanced.manager.permission.PLUGIN_HOST"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.revanced.manager.downloader
|
package app.revanced.manager.plugin.downloader
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -15,10 +15,10 @@ import kotlin.coroutines.suspendCoroutine
|
|||||||
|
|
||||||
@RequiresOptIn(
|
@RequiresOptIn(
|
||||||
level = RequiresOptIn.Level.ERROR,
|
level = RequiresOptIn.Level.ERROR,
|
||||||
message = "This API is only intended for downloader hosts, don't use it in a downloader.",
|
message = "This API is only intended for plugin hosts, don't use it in a plugin.",
|
||||||
)
|
)
|
||||||
@Retention(AnnotationRetention.BINARY)
|
@Retention(AnnotationRetention.BINARY)
|
||||||
annotation class DownloaderHostApi
|
annotation class PluginHostApi
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base interface for all DSL scopes.
|
* The base interface for all DSL scopes.
|
||||||
@@ -30,9 +30,9 @@ interface Scope {
|
|||||||
val hostPackageName: String
|
val hostPackageName: String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The package name of the downloader.
|
* The package name of the plugin.
|
||||||
*/
|
*/
|
||||||
val downloaderPackageName: String
|
val pluginPackageName: String
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,7 +43,7 @@ interface GetScope : Scope {
|
|||||||
* Ask the user to perform some required interaction in the activity specified by the provided [Intent].
|
* Ask the user to perform some required interaction in the activity specified by the provided [Intent].
|
||||||
* This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK].
|
* This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK].
|
||||||
*
|
*
|
||||||
* @throws UserInteractionException.RequestDenied User decided to skip this downloader.
|
* @throws UserInteractionException.RequestDenied User decided to skip this plugin.
|
||||||
* @throws UserInteractionException.Activity.Cancelled The activity was cancelled.
|
* @throws UserInteractionException.Activity.Cancelled The activity was cancelled.
|
||||||
* @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
|
* @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
|
||||||
*/
|
*/
|
||||||
@@ -67,14 +67,14 @@ class DownloaderScope<T : Parcelable> internal constructor(
|
|||||||
private val scopeImpl: Scope,
|
private val scopeImpl: Scope,
|
||||||
internal val context: Context
|
internal val context: Context
|
||||||
) : Scope by scopeImpl {
|
) : Scope by scopeImpl {
|
||||||
// Returning an InputStream is the primary way for downloader to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases.
|
// Returning an InputStream is the primary way for plugins to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases.
|
||||||
// It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to downloader.
|
// It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to plugins.
|
||||||
internal var download: (suspend OutputDownloadScope.(T, OutputStream) -> Unit)? = null
|
internal var download: (suspend OutputDownloadScope.(T, OutputStream) -> Unit)? = null
|
||||||
internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null
|
internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null
|
||||||
private val inputDownloadScopeImpl = object : InputDownloadScope, Scope by scopeImpl {}
|
private val inputDownloadScopeImpl = object : InputDownloadScope, Scope by scopeImpl {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the download block of the downloader.
|
* Define the download block of the plugin.
|
||||||
*/
|
*/
|
||||||
fun download(block: suspend InputDownloadScope.(data: T) -> DownloadResult) {
|
fun download(block: suspend InputDownloadScope.(data: T) -> DownloadResult) {
|
||||||
download = { app, outputStream ->
|
download = { app, outputStream ->
|
||||||
@@ -88,7 +88,7 @@ class DownloaderScope<T : Parcelable> internal constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the get block of the downloader.
|
* Define the get block of the plugin.
|
||||||
* The block should return null if the app cannot be found. The version in the result must match the version argument unless it is null.
|
* The block should return null if the app cannot be found. The version in the result must match the version argument unless it is null.
|
||||||
*/
|
*/
|
||||||
fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) {
|
fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) {
|
||||||
@@ -123,7 +123,7 @@ class DownloaderScope<T : Parcelable> internal constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) {
|
class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) {
|
||||||
@DownloaderHostApi
|
@PluginHostApi
|
||||||
fun build(scopeImpl: Scope, context: Context) =
|
fun build(scopeImpl: Scope, context: Context) =
|
||||||
with(DownloaderScope<T>(scopeImpl, context)) {
|
with(DownloaderScope<T>(scopeImpl, context)) {
|
||||||
block()
|
block()
|
||||||
@@ -136,12 +136,12 @@ class DownloaderBuilder<T : Parcelable> internal constructor(private val block:
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Downloader<T : Parcelable> internal constructor(
|
class Downloader<T : Parcelable> internal constructor(
|
||||||
@property:DownloaderHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
|
@property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
|
||||||
@property:DownloaderHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit
|
@property:PluginHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a downloader.
|
* Define a downloader plugin.
|
||||||
*/
|
*/
|
||||||
fun <T : Parcelable> Downloader(block: DownloaderScope<T>.() -> Unit) = DownloaderBuilder(block)
|
fun <T : Parcelable> Downloader(block: DownloaderScope<T>.() -> Unit) = DownloaderBuilder(block)
|
||||||
|
|
||||||
@@ -149,17 +149,17 @@ fun <T : Parcelable> Downloader(block: DownloaderScope<T>.() -> Unit) = Download
|
|||||||
* @see GetScope.requestStartActivity
|
* @see GetScope.requestStartActivity
|
||||||
*/
|
*/
|
||||||
sealed class UserInteractionException(message: String) : Exception(message) {
|
sealed class UserInteractionException(message: String) : Exception(message) {
|
||||||
class RequestDenied @DownloaderHostApi constructor() :
|
class RequestDenied @PluginHostApi constructor() :
|
||||||
UserInteractionException("Request denied by user")
|
UserInteractionException("Request denied by user")
|
||||||
|
|
||||||
sealed class Activity(message: String) : UserInteractionException(message) {
|
sealed class Activity(message: String) : UserInteractionException(message) {
|
||||||
class Cancelled @DownloaderHostApi constructor() : Activity("Interaction cancelled")
|
class Cancelled @PluginHostApi constructor() : Activity("Interaction cancelled")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param resultCode The result code of the activity.
|
* @param resultCode The result code of the activity.
|
||||||
* @param intent The [Intent] of the activity.
|
* @param intent The [Intent] of the activity.
|
||||||
*/
|
*/
|
||||||
class NotCompleted @DownloaderHostApi constructor(val resultCode: Int, val intent: Intent?) :
|
class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) :
|
||||||
Activity("Unexpected activity result code: $resultCode")
|
Activity("Unexpected activity result code: $resultCode")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.revanced.manager.downloader
|
package app.revanced.manager.plugin.downloader
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
@@ -28,7 +28,7 @@ fun <T : Parcelable> DownloaderScope<T>.download(block: suspend OutputDownloadSc
|
|||||||
*/
|
*/
|
||||||
suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity() =
|
suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity() =
|
||||||
requestStartActivity(
|
requestStartActivity(
|
||||||
Intent().apply { setClassName(downloaderPackageName, ACTIVITY::class.qualifiedName!!) }
|
Intent().apply { setClassName(pluginPackageName, ACTIVITY::class.qualifiedName!!) }
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,5 +38,5 @@ suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity()
|
|||||||
suspend inline fun <reified SERVICE : Service, R : Any?> DownloaderScope<*>.useService(
|
suspend inline fun <reified SERVICE : Service, R : Any?> DownloaderScope<*>.useService(
|
||||||
noinline block: suspend (IBinder) -> R
|
noinline block: suspend (IBinder) -> R
|
||||||
) = useService(
|
) = useService(
|
||||||
Intent().apply { setClassName(downloaderPackageName, SERVICE::class.qualifiedName!!) }, block
|
Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block
|
||||||
)
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.revanced.manager.downloader
|
package app.revanced.manager.plugin.downloader
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@@ -7,7 +7,7 @@ import java.net.URI
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple parcelable data class for storing a package name and version.
|
* A simple parcelable data class for storing a package name and version.
|
||||||
* This can be used as the data type for downloader that only need a name and version to implement their [DownloaderScope.download] function.
|
* This can be used as the data type for plugins that only need a name and version to implement their [DownloaderScope.download] function.
|
||||||
*
|
*
|
||||||
* @param name The package name.
|
* @param name The package name.
|
||||||
* @param version The version.
|
* @param version The version.
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
package app.revanced.manager.downloader.webview
|
package app.revanced.manager.plugin.downloader.webview
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import app.revanced.manager.downloader.DownloadUrl
|
import app.revanced.manager.plugin.downloader.DownloadUrl
|
||||||
import app.revanced.manager.downloader.DownloaderScope
|
import app.revanced.manager.plugin.downloader.DownloaderScope
|
||||||
import app.revanced.manager.downloader.GetScope
|
import app.revanced.manager.plugin.downloader.GetScope
|
||||||
import app.revanced.manager.downloader.Scope
|
import app.revanced.manager.plugin.downloader.Scope
|
||||||
import app.revanced.manager.downloader.Downloader
|
import app.revanced.manager.plugin.downloader.Downloader
|
||||||
import app.revanced.manager.downloader.DownloaderHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -32,7 +32,7 @@ interface WebViewCallbackScope<T> : Scope {
|
|||||||
suspend fun load(url: String)
|
suspend fun load(url: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DownloaderHostApi::class)
|
@OptIn(PluginHostApi::class)
|
||||||
class WebViewScope<T> internal constructor(
|
class WebViewScope<T> internal constructor(
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
private val scopeImpl: Scope,
|
private val scopeImpl: Scope,
|
||||||
@@ -110,7 +110,7 @@ private value class Container<U>(val value: U)
|
|||||||
* @param title The string displayed in the action bar.
|
* @param title The string displayed in the action bar.
|
||||||
* @param block The control block.
|
* @param block The control block.
|
||||||
*/
|
*/
|
||||||
@OptIn(DownloaderHostApi::class)
|
@OptIn(PluginHostApi::class)
|
||||||
suspend fun <T> GetScope.runWebView(
|
suspend fun <T> GetScope.runWebView(
|
||||||
title: String,
|
title: String,
|
||||||
block: suspend WebViewScope<T>.() -> InitialUrl
|
block: suspend WebViewScope<T>.() -> InitialUrl
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.revanced.manager.downloader.webview
|
package app.revanced.manager.plugin.downloader.webview
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -20,15 +20,15 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.downloader.DownloaderHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.downloader.R
|
import app.revanced.manager.plugin.downloader.R
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@OptIn(DownloaderHostApi::class)
|
@OptIn(PluginHostApi::class)
|
||||||
@DownloaderHostApi
|
@PluginHostApi
|
||||||
class WebViewActivity : ComponentActivity() {
|
class WebViewActivity : ComponentActivity() {
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -110,7 +110,7 @@ class WebViewActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DownloaderHostApi::class)
|
@OptIn(PluginHostApi::class)
|
||||||
internal class WebViewModel : ViewModel() {
|
internal class WebViewModel : ViewModel() {
|
||||||
init {
|
init {
|
||||||
CookieManager.getInstance().apply {
|
CookieManager.getInstance().apply {
|
||||||
@@ -1,3 +1,29 @@
|
|||||||
|
# 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)
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
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 {
|
||||||
@@ -9,6 +12,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +85,8 @@ dependencies {
|
|||||||
implementation(libs.koin.workmanager)
|
implementation(libs.koin.workmanager)
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
implementation(libs.about.libraries)
|
implementation(libs.about.libraries.core)
|
||||||
|
implementation(libs.about.libraries.m3)
|
||||||
|
|
||||||
// Ktor
|
// Ktor
|
||||||
implementation(libs.ktor.core)
|
implementation(libs.ktor.core)
|
||||||
@@ -126,7 +131,7 @@ buildscript {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.manager"
|
namespace = "app.revanced.manager"
|
||||||
compileSdk = 35
|
compileSdk = 36
|
||||||
buildToolsVersion = "35.0.1"
|
buildToolsVersion = "35.0.1"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -143,13 +148,25 @@ 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")
|
||||||
}
|
}
|
||||||
@@ -221,20 +238,14 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
androidResources {
|
||||||
androidResources {
|
generateLocaleConfig = true
|
||||||
generateLocaleConfig = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
@@ -247,6 +258,18 @@ 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.17
|
version = 1.26.0-dev.20
|
||||||
|
|||||||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@@ -1,7 +1,7 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
-keep class app.revanced.manager.patcher.runtime.process.* { *; }
|
-keep class app.revanced.manager.patcher.runtime.process.* { *; }
|
||||||
-keep class app.revanced.manager.downloader.** { *; }
|
-keep class app.revanced.manager.plugin.** { *; }
|
||||||
-keep class app.revanced.patcher.** { *; }
|
-keep class app.revanced.patcher.** { *; }
|
||||||
-keep class com.android.tools.smali.** { *; }
|
-keep class com.android.tools.smali.** { *; }
|
||||||
-keep class kotlin.** { *; }
|
-keep class kotlin.** { *; }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "9a937123afc782978d185d00c35d20e0",
|
"identityHash": "d0119047505da435972c5247181de675",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "patch_bundles",
|
"tableName": "patch_bundles",
|
||||||
@@ -21,9 +21,10 @@
|
|||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "versionHash",
|
"fieldPath": "version",
|
||||||
"columnName": "version",
|
"columnName": "version",
|
||||||
"affinity": "TEXT"
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "source",
|
"fieldPath": "source",
|
||||||
@@ -43,7 +44,9 @@
|
|||||||
"columnNames": [
|
"columnNames": [
|
||||||
"uid"
|
"uid"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "patch_selections",
|
"tableName": "patch_selections",
|
||||||
@@ -124,6 +127,7 @@
|
|||||||
"patch_name"
|
"patch_name"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"indices": [],
|
||||||
"foreignKeys": [
|
"foreignKeys": [
|
||||||
{
|
{
|
||||||
"table": "patch_selections",
|
"table": "patch_selections",
|
||||||
@@ -173,7 +177,9 @@
|
|||||||
"package_name",
|
"package_name",
|
||||||
"version"
|
"version"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "installed_app",
|
"tableName": "installed_app",
|
||||||
@@ -209,7 +215,9 @@
|
|||||||
"columnNames": [
|
"columnNames": [
|
||||||
"current_package_name"
|
"current_package_name"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "applied_patch",
|
"tableName": "applied_patch",
|
||||||
@@ -370,6 +378,7 @@
|
|||||||
"key"
|
"key"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"indices": [],
|
||||||
"foreignKeys": [
|
"foreignKeys": [
|
||||||
{
|
{
|
||||||
"table": "option_groups",
|
"table": "option_groups",
|
||||||
@@ -385,7 +394,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "trusted_downloader",
|
"tableName": "trusted_downloader_plugins",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -406,12 +415,15 @@
|
|||||||
"columnNames": [
|
"columnNames": [
|
||||||
"package_name"
|
"package_name"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9a937123afc782978d185d00c35d20e0')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd0119047505da435972c5247181de675')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<permission
|
<permission
|
||||||
android:name="app.revanced.manager.permission.DOWNLOADER_HOST"
|
android:name="app.revanced.manager.permission.PLUGIN_HOST"
|
||||||
android:protectionLevel="signature"
|
android:protectionLevel="signature"
|
||||||
android:label="@string/downloader_host_permission_label"
|
android:label="@string/plugin_host_permission_label"
|
||||||
android:description="@string/downloader_host_permission_description"
|
android:description="@string/plugin_host_permission_description"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<uses-permission android:name="app.revanced.manager.permission.DOWNLOADER_HOST" />
|
<uses-permission android:name="app.revanced.manager.permission.PLUGIN_HOST" />
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -59,11 +59,10 @@ 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 : ComponentActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -129,7 +128,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
onUpdateClick = {
|
onUpdateClick = {
|
||||||
navController.navigate(Update())
|
navController.navigate(Update())
|
||||||
},
|
},
|
||||||
onDownloaderClick = {
|
onDownloaderPluginClick = {
|
||||||
navController.navigate(Settings.Downloads)
|
navController.navigate(Settings.Downloads)
|
||||||
},
|
},
|
||||||
onAppClick = { packageName ->
|
onAppClick = { packageName ->
|
||||||
@@ -185,7 +184,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
val data =
|
val data =
|
||||||
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
||||||
val viewModel =
|
val viewModel =
|
||||||
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
koinViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||||
parametersOf(data)
|
parametersOf(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +225,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
composable<SelectedApplicationInfo.PatchesSelector> {
|
composable<SelectedApplicationInfo.PatchesSelector> {
|
||||||
val data =
|
val data =
|
||||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
|
||||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -243,7 +242,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
composable<SelectedApplicationInfo.RequiredOptions> {
|
composable<SelectedApplicationInfo.RequiredOptions> {
|
||||||
val data =
|
val data =
|
||||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
|
||||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import android.util.Log
|
|||||||
import app.revanced.manager.data.platform.Filesystem
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.di.*
|
import app.revanced.manager.di.*
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.DownloaderRepository
|
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -29,7 +29,7 @@ class ManagerApplication : Application() {
|
|||||||
private val scope = MainScope()
|
private val scope = MainScope()
|
||||||
private val prefs: PreferencesManager by inject()
|
private val prefs: PreferencesManager by inject()
|
||||||
private val patchBundleRepository: PatchBundleRepository by inject()
|
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||||
private val downloaderRepository: DownloaderRepository by inject()
|
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
|
||||||
private val fs: Filesystem by inject()
|
private val fs: Filesystem by inject()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -70,7 +70,7 @@ class ManagerApplication : Application() {
|
|||||||
prefs.preload()
|
prefs.preload()
|
||||||
}
|
}
|
||||||
scope.launch(Dispatchers.Default) {
|
scope.launch(Dispatchers.Default) {
|
||||||
downloaderRepository.reload()
|
downloaderPluginRepository.reload()
|
||||||
}
|
}
|
||||||
scope.launch(Dispatchers.Default) {
|
scope.launch(Dispatchers.Default) {
|
||||||
with(patchBundleRepository) {
|
with(patchBundleRepository) {
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
|||||||
import app.revanced.manager.data.room.options.Option
|
import app.revanced.manager.data.room.options.Option
|
||||||
import app.revanced.manager.data.room.options.OptionDao
|
import app.revanced.manager.data.room.options.OptionDao
|
||||||
import app.revanced.manager.data.room.options.OptionGroup
|
import app.revanced.manager.data.room.options.OptionGroup
|
||||||
import app.revanced.manager.data.room.downloader.TrustedDownloader
|
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
|
||||||
import app.revanced.manager.data.room.downloader.TrustedDownloaderDao
|
import app.revanced.manager.data.room.plugins.TrustedDownloaderPluginDao
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloader::class],
|
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloaderPlugin::class],
|
||||||
version = 1
|
version = 1
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
@@ -31,7 +31,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
abstract fun downloadedAppDao(): DownloadedAppDao
|
abstract fun downloadedAppDao(): DownloadedAppDao
|
||||||
abstract fun installedAppDao(): InstalledAppDao
|
abstract fun installedAppDao(): InstalledAppDao
|
||||||
abstract fun optionDao(): OptionDao
|
abstract fun optionDao(): OptionDao
|
||||||
abstract fun trustedDownloaderDao(): TrustedDownloaderDao
|
abstract fun trustedDownloaderPluginDao(): TrustedDownloaderPluginDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun generateUid() = Random.Default.nextInt()
|
fun generateUid() = Random.Default.nextInt()
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package app.revanced.manager.data.room.downloader
|
|
||||||
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Query
|
|
||||||
import androidx.room.Transaction
|
|
||||||
import androidx.room.Upsert
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface TrustedDownloaderDao {
|
|
||||||
@Query("SELECT signature FROM trusted_downloader WHERE package_name = :packageName")
|
|
||||||
suspend fun getTrustedSignature(packageName: String): ByteArray?
|
|
||||||
|
|
||||||
@Upsert
|
|
||||||
suspend fun upsertTrust(downloader: TrustedDownloader)
|
|
||||||
|
|
||||||
@Query("DELETE FROM trusted_downloader WHERE package_name = :packageName")
|
|
||||||
suspend fun remove(packageName: String)
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("DELETE FROM trusted_downloader WHERE package_name IN (:packageNames)")
|
|
||||||
suspend fun removeAll(packageNames: Set<String>)
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package app.revanced.manager.data.room.downloader
|
package app.revanced.manager.data.room.plugins
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
@Entity(tableName = "trusted_downloader")
|
@Entity(tableName = "trusted_downloader_plugins")
|
||||||
class TrustedDownloader(
|
class TrustedDownloaderPlugin(
|
||||||
@PrimaryKey @ColumnInfo(name = "package_name") val packageName: String,
|
@PrimaryKey @ColumnInfo(name = "package_name") val packageName: String,
|
||||||
@ColumnInfo(name = "signature") val signature: ByteArray
|
@ColumnInfo(name = "signature") val signature: ByteArray
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.manager.data.room.plugins
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Upsert
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface TrustedDownloaderPluginDao {
|
||||||
|
@Query("SELECT signature FROM trusted_downloader_plugins WHERE package_name = :packageName")
|
||||||
|
suspend fun getTrustedSignature(packageName: String): ByteArray?
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
suspend fun upsertTrust(plugin: TrustedDownloaderPlugin)
|
||||||
|
|
||||||
|
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name = :packageName")
|
||||||
|
suspend fun remove(packageName: String)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name IN (:packageNames)")
|
||||||
|
suspend fun removeAll(packageNames: Set<String>)
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ val repositoryModule = module {
|
|||||||
// It is best to load patch bundles ASAP
|
// It is best to load patch bundles ASAP
|
||||||
createdAtStart()
|
createdAtStart()
|
||||||
}
|
}
|
||||||
singleOf(::DownloaderRepository)
|
singleOf(::DownloaderPluginRepository)
|
||||||
singleOf(::WorkerRepository)
|
singleOf(::WorkerRepository)
|
||||||
singleOf(::DownloadedAppRepository)
|
singleOf(::DownloadedAppRepository)
|
||||||
singleOf(::InstalledAppRepository)
|
singleOf(::InstalledAppRepository)
|
||||||
|
|||||||
@@ -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.androidx.viewmodel.dsl.viewModelOf
|
import org.koin.core.module.dsl.*
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val viewModelModule = module {
|
val viewModelModule = module {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class PreferencesManager(
|
|||||||
val disableUniversalPatchCheck = booleanPreference("disable_patch_universal_check", false)
|
val disableUniversalPatchCheck = booleanPreference("disable_patch_universal_check", false)
|
||||||
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
||||||
|
|
||||||
val acknowledgedDownloader = stringSetPreference("acknowledged_downloader", emptySet())
|
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
||||||
|
|
||||||
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
|
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import android.os.Parcelable
|
|||||||
import app.revanced.manager.data.room.AppDatabase
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||||
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloader
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
import app.revanced.manager.downloader.OutputDownloadScope
|
import app.revanced.manager.plugin.downloader.OutputDownloadScope
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
@@ -36,7 +36,7 @@ class DownloadedAppRepository(
|
|||||||
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
|
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
|
||||||
|
|
||||||
suspend fun download(
|
suspend fun download(
|
||||||
downloader: LoadedDownloader,
|
plugin: LoadedDownloaderPlugin,
|
||||||
data: Parcelable,
|
data: Parcelable,
|
||||||
expectedPackageName: String,
|
expectedPackageName: String,
|
||||||
expectedVersion: String?,
|
expectedVersion: String?,
|
||||||
@@ -55,7 +55,7 @@ class DownloadedAppRepository(
|
|||||||
|
|
||||||
channelFlow {
|
channelFlow {
|
||||||
val scope = object : OutputDownloadScope {
|
val scope = object : OutputDownloadScope {
|
||||||
override val downloaderPackageName = downloader.packageName
|
override val pluginPackageName = plugin.packageName
|
||||||
override val hostPackageName = app.packageName
|
override val hostPackageName = app.packageName
|
||||||
override suspend fun reportSize(size: Long) {
|
override suspend fun reportSize(size: Long) {
|
||||||
require(size > 0) { "Size must be greater than zero" }
|
require(size > 0) { "Size must be greater than zero" }
|
||||||
@@ -87,7 +87,7 @@ class DownloadedAppRepository(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
downloader.download(scope, data, stream)
|
plugin.download(scope, data, stream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.conflate()
|
.conflate()
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||||
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
|
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||||
|
import app.revanced.manager.plugin.downloader.DownloaderBuilder
|
||||||
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
|
import app.revanced.manager.plugin.downloader.Scope
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import dalvik.system.PathClassLoader
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
|
||||||
|
@OptIn(PluginHostApi::class)
|
||||||
|
class DownloaderPluginRepository(
|
||||||
|
private val pm: PM,
|
||||||
|
private val prefs: PreferencesManager,
|
||||||
|
private val app: Application,
|
||||||
|
db: AppDatabase
|
||||||
|
) {
|
||||||
|
private val trustDao = db.trustedDownloaderPluginDao()
|
||||||
|
private val _pluginStates = MutableStateFlow(emptyMap<String, DownloaderPluginState>())
|
||||||
|
val pluginStates = _pluginStates.asStateFlow()
|
||||||
|
val loadedPluginsFlow = pluginStates.map { states ->
|
||||||
|
states.values.filterIsInstance<DownloaderPluginState.Loaded>().map { it.plugin }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val acknowledgedDownloaderPlugins = prefs.acknowledgedDownloaderPlugins
|
||||||
|
private val installedPluginPackageNames = MutableStateFlow(emptySet<String>())
|
||||||
|
val newPluginPackageNames = combine(
|
||||||
|
installedPluginPackageNames,
|
||||||
|
acknowledgedDownloaderPlugins.flow
|
||||||
|
) { installed, acknowledged ->
|
||||||
|
installed subtract acknowledged
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reload() {
|
||||||
|
val plugins =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
pm.getPackagesWithFeature(PLUGIN_FEATURE)
|
||||||
|
.associate { it.packageName to loadPlugin(it.packageName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
_pluginStates.value = plugins
|
||||||
|
installedPluginPackageNames.value = plugins.keys
|
||||||
|
|
||||||
|
val acknowledgedPlugins = acknowledgedDownloaderPlugins.get()
|
||||||
|
val uninstalledPlugins = acknowledgedPlugins subtract installedPluginPackageNames.value
|
||||||
|
if (uninstalledPlugins.isNotEmpty()) {
|
||||||
|
Log.d(tag, "Uninstalled plugins: ${uninstalledPlugins.joinToString(", ")}")
|
||||||
|
acknowledgedDownloaderPlugins.update(acknowledgedPlugins subtract uninstalledPlugins)
|
||||||
|
trustDao.removeAll(uninstalledPlugins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unwrapParceledData(data: ParceledDownloaderData): Pair<LoadedDownloaderPlugin, Parcelable> {
|
||||||
|
val plugin =
|
||||||
|
(_pluginStates.value[data.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin
|
||||||
|
?: throw Exception("Downloader plugin with name ${data.pluginPackageName} is not available")
|
||||||
|
|
||||||
|
return plugin to data.unwrapWith(plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadPlugin(packageName: String): DownloaderPluginState {
|
||||||
|
try {
|
||||||
|
if (!verify(packageName)) return DownloaderPluginState.Untrusted
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(tag, "Got exception while verifying plugin $packageName", e)
|
||||||
|
return DownloaderPluginState.Failed(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!!
|
||||||
|
val className = packageInfo.applicationInfo!!.metaData.getString(METADATA_PLUGIN_CLASS)
|
||||||
|
?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS")
|
||||||
|
|
||||||
|
val classLoader =
|
||||||
|
PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader)
|
||||||
|
val pluginContext = app.createPackageContext(packageName, 0)
|
||||||
|
|
||||||
|
val downloader = classLoader
|
||||||
|
.loadClass(className)
|
||||||
|
.getDownloaderBuilder()
|
||||||
|
.build(
|
||||||
|
scopeImpl = object : Scope {
|
||||||
|
override val hostPackageName = app.packageName
|
||||||
|
override val pluginPackageName = pluginContext.packageName
|
||||||
|
},
|
||||||
|
context = pluginContext
|
||||||
|
)
|
||||||
|
|
||||||
|
DownloaderPluginState.Loaded(
|
||||||
|
LoadedDownloaderPlugin(
|
||||||
|
packageName,
|
||||||
|
with(pm) { packageInfo.label() },
|
||||||
|
packageInfo.versionName!!,
|
||||||
|
downloader.get,
|
||||||
|
downloader.download,
|
||||||
|
classLoader
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.e(tag, "Failed to load plugin $packageName", t)
|
||||||
|
DownloaderPluginState.Failed(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun trustPackage(packageName: String) {
|
||||||
|
trustDao.upsertTrust(
|
||||||
|
TrustedDownloaderPlugin(
|
||||||
|
packageName,
|
||||||
|
pm.getSignature(packageName).toByteArray()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
reload()
|
||||||
|
prefs.edit {
|
||||||
|
acknowledgedDownloaderPlugins += packageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun revokeTrustForPackage(packageName: String) =
|
||||||
|
trustDao.remove(packageName).also { reload() }
|
||||||
|
|
||||||
|
suspend fun acknowledgeAllNewPlugins() =
|
||||||
|
acknowledgedDownloaderPlugins.update(installedPluginPackageNames.value)
|
||||||
|
|
||||||
|
private suspend fun verify(packageName: String): Boolean {
|
||||||
|
val expectedSignature =
|
||||||
|
trustDao.getTrustedSignature(packageName) ?: return false
|
||||||
|
|
||||||
|
return pm.hasSignature(packageName, expectedSignature)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val PLUGIN_FEATURE = "app.revanced.manager.plugin.downloader"
|
||||||
|
const val METADATA_PLUGIN_CLASS = "app.revanced.manager.plugin.downloader.class"
|
||||||
|
|
||||||
|
const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
|
||||||
|
val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
|
||||||
|
val Class<*>.isDownloaderBuilder get() = DownloaderBuilder::class.java.isAssignableFrom(this)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun Class<*>.getDownloaderBuilder() =
|
||||||
|
declaredMethods
|
||||||
|
.firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() }
|
||||||
|
?.let { it(null) as DownloaderBuilder<Parcelable> }
|
||||||
|
?: throw Exception("Could not find a valid downloader implementation in class $canonicalName")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
package app.revanced.manager.domain.repository
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.util.Log
|
|
||||||
import app.revanced.manager.data.room.AppDatabase
|
|
||||||
import app.revanced.manager.data.room.downloader.TrustedDownloader
|
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
|
||||||
import app.revanced.manager.network.downloader.DownloaderPackageState
|
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloader
|
|
||||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
|
||||||
import app.revanced.manager.downloader.DownloaderBuilder
|
|
||||||
import app.revanced.manager.downloader.DownloaderHostApi
|
|
||||||
import app.revanced.manager.downloader.Scope
|
|
||||||
import app.revanced.manager.util.PM
|
|
||||||
import app.revanced.manager.util.tag
|
|
||||||
import dalvik.system.PathClassLoader
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.lang.reflect.Modifier
|
|
||||||
|
|
||||||
@OptIn(DownloaderHostApi::class)
|
|
||||||
class DownloaderRepository(
|
|
||||||
private val pm: PM,
|
|
||||||
private val prefs: PreferencesManager,
|
|
||||||
private val app: Application,
|
|
||||||
db: AppDatabase
|
|
||||||
) {
|
|
||||||
private val trustDao = db.trustedDownloaderDao()
|
|
||||||
private val _downloaderPackageStates = MutableStateFlow(emptyMap<String, DownloaderPackageState>())
|
|
||||||
val downloaderPackageStates = _downloaderPackageStates.asStateFlow()
|
|
||||||
val loadedDownloaderPackageFlow = downloaderPackageStates.map { states ->
|
|
||||||
states.values.filterIsInstance<DownloaderPackageState.Loaded>().flatMap { it.downloader }
|
|
||||||
}
|
|
||||||
|
|
||||||
private val acknowledgedPackageDownloader = prefs.acknowledgedDownloader
|
|
||||||
private val installedDownloaderPackageNames = MutableStateFlow(emptySet<String>())
|
|
||||||
val newDownloaderPackageNames = combine(
|
|
||||||
installedDownloaderPackageNames,
|
|
||||||
acknowledgedPackageDownloader.flow
|
|
||||||
) { installed, acknowledged ->
|
|
||||||
installed subtract acknowledged
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun reload() {
|
|
||||||
val downloaderPackages =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
pm.getPackagesWithFeature(DOWNLOADER_FEATURE)
|
|
||||||
.associate { it.packageName to loadDownloader(it.packageName) }
|
|
||||||
}
|
|
||||||
|
|
||||||
_downloaderPackageStates.value = downloaderPackages
|
|
||||||
installedDownloaderPackageNames.value = downloaderPackages.keys
|
|
||||||
|
|
||||||
val acknowledgedDownloader = this@DownloaderRepository.acknowledgedPackageDownloader.get()
|
|
||||||
val uninstalledDownloader = acknowledgedDownloader subtract installedDownloaderPackageNames.value
|
|
||||||
if (uninstalledDownloader.isNotEmpty()) {
|
|
||||||
Log.d(tag, "Uninstalled downloader: ${uninstalledDownloader.joinToString(", ")}")
|
|
||||||
this@DownloaderRepository.acknowledgedPackageDownloader.update(acknowledgedDownloader subtract uninstalledDownloader)
|
|
||||||
trustDao.removeAll(uninstalledDownloader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unwrapParceledData(data: ParceledDownloaderData): Pair<LoadedDownloader, Parcelable> {
|
|
||||||
val downloader =
|
|
||||||
(_downloaderPackageStates.value[data.downloaderPackageName] as? DownloaderPackageState.Loaded)
|
|
||||||
?.downloader?.first { it.name == data.downloaderName }
|
|
||||||
?: throw Exception("Downloader package name ${data.downloaderPackageName} is not available")
|
|
||||||
|
|
||||||
return downloader to data.unwrapWith(downloader)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadDownloader(packageName: String): DownloaderPackageState {
|
|
||||||
try {
|
|
||||||
if (!verify(packageName)) return DownloaderPackageState.Untrusted
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(tag, "Got exception while verifying downloader $packageName", e)
|
|
||||||
return DownloaderPackageState.Failed(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
|
||||||
val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!!
|
|
||||||
val classNames = packageInfo.applicationInfo!!.metaData.getStringArray(METADATA_DOWNLOADER_CLASSES)
|
|
||||||
?: throw Exception("Missing metadata attribute $METADATA_DOWNLOADER_CLASSES")
|
|
||||||
|
|
||||||
val classLoader =
|
|
||||||
PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader)
|
|
||||||
val downloaderContext = app.createPackageContext(packageName, 0)
|
|
||||||
|
|
||||||
val scopeImpl = object : Scope {
|
|
||||||
override val hostPackageName = app.packageName
|
|
||||||
override val downloaderPackageName = downloaderContext.packageName
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloaderPackageState.Loaded(
|
|
||||||
classNames.map { className ->
|
|
||||||
val downloader = classLoader
|
|
||||||
.loadClass(className)
|
|
||||||
.getDownloaderBuilder()
|
|
||||||
.build(
|
|
||||||
scopeImpl = scopeImpl,
|
|
||||||
context = downloaderContext
|
|
||||||
)
|
|
||||||
|
|
||||||
LoadedDownloader(
|
|
||||||
packageName,
|
|
||||||
with(pm) { packageInfo.label() },
|
|
||||||
packageInfo.versionName!!,
|
|
||||||
downloader.get,
|
|
||||||
downloader.download,
|
|
||||||
classLoader
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Log.e(tag, "Failed to load downloader $packageName", t)
|
|
||||||
DownloaderPackageState.Failed(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun trustPackage(packageName: String) {
|
|
||||||
trustDao.upsertTrust(
|
|
||||||
TrustedDownloader(
|
|
||||||
packageName,
|
|
||||||
pm.getSignature(packageName).toByteArray()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
reload()
|
|
||||||
prefs.edit {
|
|
||||||
acknowledgedPackageDownloader += packageName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun revokeTrustForPackage(packageName: String) =
|
|
||||||
trustDao.remove(packageName).also { reload() }
|
|
||||||
|
|
||||||
suspend fun acknowledgeAllNewDownloader() =
|
|
||||||
acknowledgedPackageDownloader.update(installedDownloaderPackageNames.value)
|
|
||||||
|
|
||||||
private suspend fun verify(packageName: String): Boolean {
|
|
||||||
val expectedSignature =
|
|
||||||
trustDao.getTrustedSignature(packageName) ?: return false
|
|
||||||
|
|
||||||
return pm.hasSignature(packageName, expectedSignature)
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val DOWNLOADER_FEATURE = "app.revanced.manager.downloader"
|
|
||||||
const val METADATA_DOWNLOADER_CLASSES = "app.revanced.manager.downloader.classes"
|
|
||||||
|
|
||||||
const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
|
|
||||||
val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
|
|
||||||
val Class<*>.isDownloaderBuilder get() = DownloaderBuilder::class.java.isAssignableFrom(this)
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun Class<*>.getDownloaderBuilder() =
|
|
||||||
declaredMethods
|
|
||||||
.firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() }
|
|
||||||
?.let { it(null) as DownloaderBuilder<Parcelable> }
|
|
||||||
?: throw Exception("Could not find a valid downloader implementation in class $canonicalName")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package app.revanced.manager.network.downloader
|
|
||||||
|
|
||||||
sealed interface DownloaderPackageState {
|
|
||||||
data object Untrusted : DownloaderPackageState
|
|
||||||
|
|
||||||
data class Loaded(val downloader: List<LoadedDownloader>) : DownloaderPackageState
|
|
||||||
|
|
||||||
data class Failed(val throwable: Throwable) : DownloaderPackageState
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.manager.network.downloader
|
||||||
|
|
||||||
|
sealed interface DownloaderPluginState {
|
||||||
|
data object Untrusted : DownloaderPluginState
|
||||||
|
|
||||||
|
data class Loaded(val plugin: LoadedDownloaderPlugin) : DownloaderPluginState
|
||||||
|
|
||||||
|
data class Failed(val throwable: Throwable) : DownloaderPluginState
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package app.revanced.manager.network.downloader
|
package app.revanced.manager.network.downloader
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import app.revanced.manager.downloader.OutputDownloadScope
|
import app.revanced.manager.plugin.downloader.OutputDownloadScope
|
||||||
import app.revanced.manager.downloader.GetScope
|
import app.revanced.manager.plugin.downloader.GetScope
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
class LoadedDownloader(
|
class LoadedDownloaderPlugin(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val version: String,
|
val version: String,
|
||||||
@@ -10,22 +10,20 @@ import kotlinx.parcelize.Parcelize
|
|||||||
* A container for [Parcelable] data returned from downloader. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
|
* A container for [Parcelable] data returned from downloader. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
|
||||||
*/
|
*/
|
||||||
class ParceledDownloaderData private constructor(
|
class ParceledDownloaderData private constructor(
|
||||||
val downloaderPackageName: String,
|
val pluginPackageName: String,
|
||||||
val downloaderName: String,
|
|
||||||
private val bundle: Bundle
|
private val bundle: Bundle
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
constructor(downloader: LoadedDownloader, data: Parcelable) : this(
|
constructor(plugin: LoadedDownloaderPlugin, data: Parcelable) : this(
|
||||||
downloader.packageName,
|
plugin.packageName,
|
||||||
downloader.name,
|
|
||||||
createBundle(data)
|
createBundle(data)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun unwrapWith(downloader: LoadedDownloader): Parcelable {
|
fun unwrapWith(plugin: LoadedDownloaderPlugin): Parcelable {
|
||||||
bundle.classLoader = downloader.classLoader
|
bundle.classLoader = plugin.classLoader
|
||||||
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
val className = bundle.getString(CLASS_NAME_KEY)!!
|
val className = bundle.getString(CLASS_NAME_KEY)!!
|
||||||
val clazz = downloader.classLoader.loadClass(className)
|
val clazz = plugin.classLoader.loadClass(className)
|
||||||
|
|
||||||
bundle.getParcelable(DATA_KEY, clazz)!! as Parcelable
|
bundle.getParcelable(DATA_KEY, clazz)!! as Parcelable
|
||||||
} else @Suppress("Deprecation") bundle.getParcelable(DATA_KEY)!!
|
} else @Suppress("Deprecation") bundle.getParcelable(DATA_KEY)!!
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ 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
|
||||||
@@ -69,14 +72,12 @@ 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) {
|
||||||
while (!channel.isClosedForRead) {
|
val channel: ByteReadChannel = httpResponse.body()
|
||||||
|
val sink = outputStream.asSink()
|
||||||
|
while (!channel.exhausted()) {
|
||||||
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||||
while (packet.isNotEmpty) {
|
packet.transferTo(sink)
|
||||||
val bytes = packet.readBytes()
|
|
||||||
outputStream.write(bytes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ import app.revanced.manager.domain.installer.RootInstaller
|
|||||||
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.DownloadedAppRepository
|
||||||
import app.revanced.manager.domain.repository.DownloaderRepository
|
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.worker.Worker
|
import app.revanced.manager.domain.worker.Worker
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloader
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
import app.revanced.manager.patcher.ProgressEvent
|
import app.revanced.manager.patcher.ProgressEvent
|
||||||
import app.revanced.manager.patcher.StepId
|
import app.revanced.manager.patcher.StepId
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
@@ -36,9 +36,9 @@ import app.revanced.manager.patcher.runStep
|
|||||||
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
||||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||||
import app.revanced.manager.patcher.toRemoteError
|
import app.revanced.manager.patcher.toRemoteError
|
||||||
import app.revanced.manager.downloader.GetScope
|
import app.revanced.manager.plugin.downloader.GetScope
|
||||||
import app.revanced.manager.downloader.DownloaderHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.downloader.UserInteractionException
|
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
@@ -51,7 +51,7 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@OptIn(DownloaderHostApi::class)
|
@OptIn(PluginHostApi::class)
|
||||||
class PatcherWorker(
|
class PatcherWorker(
|
||||||
context: Context,
|
context: Context,
|
||||||
parameters: WorkerParameters
|
parameters: WorkerParameters
|
||||||
@@ -59,7 +59,7 @@ class PatcherWorker(
|
|||||||
private val workerRepository: WorkerRepository by inject()
|
private val workerRepository: WorkerRepository by inject()
|
||||||
private val prefs: PreferencesManager by inject()
|
private val prefs: PreferencesManager by inject()
|
||||||
private val keystoreManager: KeystoreManager by inject()
|
private val keystoreManager: KeystoreManager by inject()
|
||||||
private val downloaderRepository: DownloaderRepository by inject()
|
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
|
||||||
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
||||||
private val pm: PM by inject()
|
private val pm: PM by inject()
|
||||||
private val fs: Filesystem by inject()
|
private val fs: Filesystem by inject()
|
||||||
@@ -72,7 +72,7 @@ class PatcherWorker(
|
|||||||
val selectedPatches: PatchSelection,
|
val selectedPatches: PatchSelection,
|
||||||
val options: Options,
|
val options: Options,
|
||||||
val logger: Logger,
|
val logger: Logger,
|
||||||
val handleStartActivityRequest: suspend (LoadedDownloader, 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,
|
||||||
) {
|
) {
|
||||||
@@ -150,9 +150,9 @@ class PatcherWorker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun download(downloader: LoadedDownloader, data: Parcelable) =
|
suspend fun download(plugin: LoadedDownloaderPlugin, data: Parcelable) =
|
||||||
downloadedAppRepository.download(
|
downloadedAppRepository.download(
|
||||||
downloader,
|
plugin,
|
||||||
data,
|
data,
|
||||||
args.packageName,
|
args.packageName,
|
||||||
args.input.version,
|
args.input.version,
|
||||||
@@ -172,27 +172,27 @@ class PatcherWorker(
|
|||||||
val inputFile = when (val selectedApp = args.input) {
|
val inputFile = when (val selectedApp = args.input) {
|
||||||
is SelectedApp.Download -> {
|
is SelectedApp.Download -> {
|
||||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||||
val (downloader, data) = downloaderRepository.unwrapParceledData(
|
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(
|
||||||
selectedApp.data
|
selectedApp.data
|
||||||
)
|
)
|
||||||
|
|
||||||
download(downloader, data)
|
download(plugin, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedApp.Search -> {
|
is SelectedApp.Search -> {
|
||||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||||
downloaderRepository.loadedDownloaderPackageFlow.first()
|
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||||
.firstNotNullOfOrNull { downloader ->
|
.firstNotNullOfOrNull { plugin ->
|
||||||
try {
|
try {
|
||||||
val getScope = object : GetScope {
|
val getScope = object : GetScope {
|
||||||
override val downloaderPackageName = downloader.packageName
|
override val pluginPackageName = plugin.packageName
|
||||||
override val hostPackageName =
|
override val hostPackageName =
|
||||||
applicationContext.packageName
|
applicationContext.packageName
|
||||||
|
|
||||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||||
val result =
|
val result =
|
||||||
args.handleStartActivityRequest(downloader, intent)
|
args.handleStartActivityRequest(plugin, intent)
|
||||||
return when (result.resultCode) {
|
return when (result.resultCode) {
|
||||||
Activity.RESULT_OK -> result.data
|
Activity.RESULT_OK -> result.data
|
||||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||||
@@ -204,7 +204,7 @@ class PatcherWorker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
downloader.get(
|
plugin.get(
|
||||||
getScope,
|
getScope,
|
||||||
selectedApp.packageName,
|
selectedApp.packageName,
|
||||||
selectedApp.version
|
selectedApp.version
|
||||||
@@ -214,7 +214,7 @@ class PatcherWorker(
|
|||||||
throw e
|
throw e
|
||||||
} catch (_: UserInteractionException) {
|
} catch (_: UserInteractionException) {
|
||||||
null
|
null
|
||||||
}?.let { (data, _) -> download(downloader, data) }
|
}?.let { (data, _) -> download(plugin, data) }
|
||||||
} ?: throw Exception("App is not available.")
|
} ?: throw Exception("App is not available.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,21 +250,11 @@ class PatcherWorker(
|
|||||||
tag,
|
tag,
|
||||||
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
|
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
|
||||||
)
|
)
|
||||||
args.onEvent(
|
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
||||||
ProgressEvent.Failed(
|
|
||||||
null,
|
|
||||||
e.toRemoteError()
|
|
||||||
)
|
|
||||||
) // Fallback if exception doesn't occur within step
|
|
||||||
Result.failure()
|
Result.failure()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(tag, "An exception occurred while patching".logFmt(), e)
|
Log.e(tag, "An exception occurred while patching".logFmt(), e)
|
||||||
args.onEvent(
|
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
||||||
ProgressEvent.Failed(
|
|
||||||
null,
|
|
||||||
e.toRemoteError()
|
|
||||||
)
|
|
||||||
) // Fallback if exception doesn't occur within step
|
|
||||||
Result.failure()
|
Result.failure()
|
||||||
} finally {
|
} finally {
|
||||||
patchedApk.delete()
|
patchedApk.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 io.github.fornewid.placeholder.material3.placeholder
|
import com.eygraber.compose.placeholder.material3.placeholder
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppIcon(
|
fun AppIcon(
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ 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 app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import io.github.fornewid.placeholder.material3.placeholder
|
import com.eygraber.compose.placeholder.material3.placeholder
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
|||||||
@@ -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.circularTrackColor,
|
trackColor: Color = ProgressIndicatorDefaults.circularIndeterminateTrackColor,
|
||||||
strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap
|
strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap
|
||||||
) {
|
) {
|
||||||
progress()?.let {
|
progress()?.let {
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ 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,7 +29,8 @@ 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,7 +31,8 @@ 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(@StringRes val displayName: Int) {
|
enum class StepCategory(@param: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)
|
||||||
|
|||||||
@@ -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.TabRow
|
import androidx.compose.material3.SecondaryTabRow
|
||||||
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,6 +53,7 @@ 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
|
||||||
@@ -90,16 +91,17 @@ fun DashboardScreen(
|
|||||||
onAppSelectorClick: () -> Unit,
|
onAppSelectorClick: () -> Unit,
|
||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit,
|
||||||
onUpdateClick: () -> Unit,
|
onUpdateClick: () -> Unit,
|
||||||
onDownloaderClick: () -> Unit,
|
onDownloaderPluginClick: () -> Unit,
|
||||||
onAppClick: (String) -> Unit
|
onAppClick: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
var selectedSourceCount by rememberSaveable { mutableIntStateOf(0) }
|
var selectedSourceCount by rememberSaveable { mutableIntStateOf(0) }
|
||||||
val bundlesSelectable by remember { derivedStateOf { selectedSourceCount > 0 } }
|
val bundlesSelectable by remember { derivedStateOf { selectedSourceCount > 0 } }
|
||||||
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
||||||
val showNewDownloaderNotification by vm.newDownloaderAvailable.collectAsStateWithLifecycle(
|
val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
|
||||||
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,
|
||||||
@@ -234,7 +236,7 @@ fun DashboardScreen(
|
|||||||
when (pagerState.currentPage) {
|
when (pagerState.currentPage) {
|
||||||
DashboardPage.DASHBOARD.ordinal -> {
|
DashboardPage.DASHBOARD.ordinal -> {
|
||||||
if (availablePatches < 1) {
|
if (availablePatches < 1) {
|
||||||
androidContext.toast(androidContext.getString(R.string.no_patch_found))
|
androidContext.toast(resources.getString(R.string.no_patch_found))
|
||||||
composableScope.launch {
|
composableScope.launch {
|
||||||
pagerState.animateScrollToPage(
|
pagerState.animateScrollToPage(
|
||||||
DashboardPage.BUNDLES.ordinal
|
DashboardPage.BUNDLES.ordinal
|
||||||
@@ -259,7 +261,7 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(Modifier.padding(paddingValues)) {
|
Column(Modifier.padding(paddingValues)) {
|
||||||
TabRow(
|
SecondaryTabRow(
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
) {
|
) {
|
||||||
@@ -307,14 +309,14 @@ fun DashboardScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
if (showNewDownloaderNotification) {
|
if (showNewDownloaderPluginsNotification) {
|
||||||
{
|
{
|
||||||
NotificationCard(
|
NotificationCard(
|
||||||
text = stringResource(R.string.new_downloader_notification),
|
text = stringResource(R.string.new_downloader_plugins_notification),
|
||||||
icon = Icons.Outlined.Download,
|
icon = Icons.Outlined.Download,
|
||||||
modifier = Modifier.clickable(onClick = onDownloaderClick),
|
modifier = Modifier.clickable(onClick = onDownloaderPluginClick),
|
||||||
actions = {
|
actions = {
|
||||||
TextButton(onClick = vm::ignoreNewDownloader) {
|
TextButton(onClick = vm::ignoreNewDownloaderPlugins) {
|
||||||
Text(stringResource(R.string.dismiss))
|
Text(stringResource(R.string.dismiss))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ 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
|
||||||
@@ -69,6 +70,7 @@ 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)
|
||||||
|
|
||||||
@@ -79,7 +81,7 @@ fun PatcherScreen(
|
|||||||
|
|
||||||
fun onPageBack() = when {
|
fun onPageBack() = when {
|
||||||
patcherSucceeded == null -> showDismissConfirmationDialog = true
|
patcherSucceeded == null -> showDismissConfirmationDialog = true
|
||||||
viewModel.isInstalling -> context.toast(context.getString(R.string.patcher_install_in_progress))
|
viewModel.isInstalling -> context.toast(resources.getString(R.string.patcher_install_in_progress))
|
||||||
else -> onLeave()
|
else -> onLeave()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +150,7 @@ fun PatcherScreen(
|
|||||||
},
|
},
|
||||||
title = { Text(title) },
|
title = { Text(title) },
|
||||||
text = {
|
text = {
|
||||||
Text(stringResource(R.string.downloader_activity_dialog_body))
|
Text(stringResource(R.string.plugin_activity_dialog_body))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.ScrollableTabRow
|
import androidx.compose.material3.SecondaryScrollableTabRow
|
||||||
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,10 +49,12 @@ 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
|
||||||
@@ -81,9 +83,11 @@ 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)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, FlowPreview::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PatchesSelectorScreen(
|
fun PatchesSelectorScreen(
|
||||||
onSave: (PatchSelection?, Options) -> Unit,
|
onSave: (PatchSelection?, Options) -> Unit,
|
||||||
@@ -231,7 +235,8 @@ 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 = true
|
patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning =
|
||||||
|
true
|
||||||
|
|
||||||
// Toggle the patch otherwise
|
// Toggle the patch otherwise
|
||||||
else -> viewModel.togglePatch(uid, patch)
|
else -> viewModel.togglePatch(uid, patch)
|
||||||
@@ -360,6 +365,21 @@ 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(
|
||||||
@@ -375,8 +395,7 @@ fun PatchesSelectorScreen(
|
|||||||
contentDescription = stringResource(R.string.save)
|
contentDescription = stringResource(R.string.save)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
|
expanded = expanded,
|
||||||
?: true,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onSave(viewModel.getCustomSelection(), viewModel.getOptions())
|
onSave(viewModel.getCustomSelection(), viewModel.getOptions())
|
||||||
}
|
}
|
||||||
@@ -392,7 +411,7 @@ fun PatchesSelectorScreen(
|
|||||||
.padding(top = 16.dp)
|
.padding(top = 16.dp)
|
||||||
) {
|
) {
|
||||||
if (bundles.size > 1) {
|
if (bundles.size > 1) {
|
||||||
ScrollableTabRow(
|
SecondaryScrollableTabRow(
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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.ScrollableTabRow
|
import androidx.compose.material3.SecondaryScrollableTabRow
|
||||||
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) ScrollableTabRow(
|
else if (list.size > 1) SecondaryScrollableTabRow(
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
import android.R.attr.name
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@@ -33,6 +32,7 @@ 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
|
||||||
@@ -40,7 +40,7 @@ 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.InstallType
|
||||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloader
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
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
|
||||||
@@ -69,6 +69,7 @@ fun SelectedAppInfoScreen(
|
|||||||
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() }
|
||||||
@@ -87,7 +88,7 @@ fun SelectedAppInfoScreen(
|
|||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(
|
val launcher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult(),
|
contract = ActivityResultContracts.StartActivityForResult(),
|
||||||
onResult = vm::handleDownloaderActivityResult
|
onResult = vm::handlePluginActivityResult
|
||||||
)
|
)
|
||||||
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
||||||
launcher.launch(intent)
|
launcher.launch(intent)
|
||||||
@@ -119,7 +120,7 @@ fun SelectedAppInfoScreen(
|
|||||||
},
|
},
|
||||||
onClick = patchClick@{
|
onClick = patchClick@{
|
||||||
if (selectedPatchCount == 0) {
|
if (selectedPatchCount == 0) {
|
||||||
context.toast(context.getString(R.string.no_patches_selected))
|
context.toast(resources.getString(R.string.no_patches_selected))
|
||||||
|
|
||||||
return@patchClick
|
return@patchClick
|
||||||
}
|
}
|
||||||
@@ -141,22 +142,22 @@ fun SelectedAppInfoScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
val downloader by vm.downloader.collectAsStateWithLifecycle(emptyList())
|
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
|
||||||
|
|
||||||
if (vm.showSourceSelector) {
|
if (vm.showSourceSelector) {
|
||||||
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
|
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
AppSourceSelectorDialog(
|
AppSourceSelectorDialog(
|
||||||
downloader = downloader,
|
plugins = plugins,
|
||||||
installedApp = vm.installedAppData,
|
installedApp = vm.installedAppData,
|
||||||
searchApp = SelectedApp.Search(
|
searchApp = SelectedApp.Search(
|
||||||
vm.packageName,
|
vm.packageName,
|
||||||
vm.desiredVersion
|
vm.desiredVersion
|
||||||
),
|
),
|
||||||
activeSearchJob = vm.activeDownloaderAction,
|
activeSearchJob = vm.activePluginAction,
|
||||||
hasRoot = vm.hasRoot,
|
hasRoot = vm.hasRoot,
|
||||||
onDismissRequest = vm::dismissSourceSelector,
|
onDismissRequest = vm::dismissSourceSelector,
|
||||||
onSelectDownloader = vm::searchUsingDownloader,
|
onSelectPlugin = vm::searchUsingPlugin,
|
||||||
requiredVersion = requiredVersion,
|
requiredVersion = requiredVersion,
|
||||||
onSelect = {
|
onSelect = {
|
||||||
vm.selectedApp = it
|
vm.selectedApp = it
|
||||||
@@ -202,8 +203,8 @@ fun SelectedAppInfoScreen(
|
|||||||
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
|
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
|
||||||
is SelectedApp.Download -> stringResource(
|
is SelectedApp.Download -> stringResource(
|
||||||
R.string.apk_source_downloader,
|
R.string.apk_source_downloader,
|
||||||
downloader.find { it.packageName == app.data.downloaderPackageName && it.name == app.data.downloaderName }?.let { "${it.packageName} ${it.name}" }
|
plugins.find { it.packageName == app.data.pluginPackageName }?.name
|
||||||
?: app.data.downloaderPackageName
|
?: app.data.pluginPackageName
|
||||||
)
|
)
|
||||||
|
|
||||||
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
|
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
|
||||||
@@ -280,14 +281,14 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () ->
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppSourceSelectorDialog(
|
private fun AppSourceSelectorDialog(
|
||||||
downloader: List<LoadedDownloader>,
|
plugins: List<LoadedDownloaderPlugin>,
|
||||||
installedApp: Pair<SelectedApp.Installed, InstalledApp?>?,
|
installedApp: Pair<SelectedApp.Installed, InstalledApp?>?,
|
||||||
searchApp: SelectedApp.Search,
|
searchApp: SelectedApp.Search,
|
||||||
activeSearchJob: String?,
|
activeSearchJob: String?,
|
||||||
hasRoot: Boolean,
|
hasRoot: Boolean,
|
||||||
requiredVersion: String?,
|
requiredVersion: String?,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onSelectDownloader: (LoadedDownloader) -> Unit,
|
onSelectPlugin: (LoadedDownloaderPlugin) -> Unit,
|
||||||
onSelect: (SelectedApp) -> Unit,
|
onSelect: (SelectedApp) -> Unit,
|
||||||
) {
|
) {
|
||||||
val canSelect = activeSearchJob == null
|
val canSelect = activeSearchJob == null
|
||||||
@@ -304,15 +305,15 @@ private fun AppSourceSelectorDialog(
|
|||||||
text = {
|
text = {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
item(key = "auto") {
|
item(key = "auto") {
|
||||||
val hasDownloader = downloader.isNotEmpty()
|
val hasPlugins = plugins.isNotEmpty()
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(enabled = canSelect && hasDownloader) { onSelect(searchApp) }
|
.clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) }
|
||||||
.enabled(hasDownloader),
|
.enabled(hasPlugins),
|
||||||
headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) },
|
headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) },
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(
|
Text(
|
||||||
if (hasDownloader)
|
if (hasPlugins)
|
||||||
stringResource(R.string.app_source_dialog_option_auto_description)
|
stringResource(R.string.app_source_dialog_option_auto_description)
|
||||||
else
|
else
|
||||||
stringResource(R.string.app_source_dialog_option_auto_unavailable)
|
stringResource(R.string.app_source_dialog_option_auto_unavailable)
|
||||||
@@ -350,11 +351,11 @@ private fun AppSourceSelectorDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(downloader, key = { "downloader_${it.packageName}" }) { downloader ->
|
items(plugins, key = { "plugin_${it.packageName}" }) { plugin ->
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable(enabled = canSelect) { onSelectDownloader(downloader) },
|
modifier = Modifier.clickable(enabled = canSelect) { onSelectPlugin(plugin) },
|
||||||
headlineContent = { Text(downloader.name) },
|
headlineContent = { Text(plugin.name) },
|
||||||
trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == downloader.packageName },
|
trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == plugin.packageName },
|
||||||
colors = transparentListItemColors
|
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(
|
||||||
@StringRes val name: Int,
|
@param:StringRes val name: Int,
|
||||||
@StringRes val description: Int,
|
@param:StringRes val description: Int,
|
||||||
val image: ImageVector,
|
val image: ImageVector,
|
||||||
val destination: Settings.Destination,
|
val destination: Settings.Destination,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -40,6 +41,7 @@ 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
|
||||||
@@ -67,8 +69,9 @@ 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 {
|
val icon = rememberDrawablePainter(drawable = remember(resources) {
|
||||||
AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring)
|
AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -76,7 +79,7 @@ fun AboutSettingsScreen(
|
|||||||
viewModel.socials.partition(ReVancedSocial::preferred)
|
viewModel.socials.partition(ReVancedSocial::preferred)
|
||||||
}
|
}
|
||||||
|
|
||||||
val preferredSocialButtons = remember(preferredSocials, viewModel.donate, viewModel.contact) {
|
val preferredSocialButtons = remember(resources, preferredSocials, viewModel.donate, viewModel.contact) {
|
||||||
preferredSocials.map {
|
preferredSocials.map {
|
||||||
Triple(
|
Triple(
|
||||||
getSocialIcon(it.name),
|
getSocialIcon(it.name),
|
||||||
@@ -89,7 +92,7 @@ fun AboutSettingsScreen(
|
|||||||
viewModel.donate?.let {
|
viewModel.donate?.let {
|
||||||
Triple(
|
Triple(
|
||||||
Icons.Outlined.FavoriteBorder,
|
Icons.Outlined.FavoriteBorder,
|
||||||
context.getString(R.string.donate),
|
resources.getString(R.string.donate),
|
||||||
third = {
|
third = {
|
||||||
context.openUrl(it)
|
context.openUrl(it)
|
||||||
}
|
}
|
||||||
@@ -98,7 +101,7 @@ fun AboutSettingsScreen(
|
|||||||
viewModel.contact?.let {
|
viewModel.contact?.let {
|
||||||
Triple(
|
Triple(
|
||||||
Icons.Outlined.MailOutline,
|
Icons.Outlined.MailOutline,
|
||||||
context.getString(R.string.contact),
|
resources.getString(R.string.contact),
|
||||||
third = {
|
third = {
|
||||||
context.openUrl("mailto:$it")
|
context.openUrl("mailto:$it")
|
||||||
}
|
}
|
||||||
@@ -131,7 +134,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(context.getString(R.string.no_network_toast))
|
context.toast(resources.getString(R.string.no_network_toast))
|
||||||
return@nav
|
return@nav
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +156,7 @@ fun AboutSettingsScreen(
|
|||||||
LaunchedEffect(developerTaps) {
|
LaunchedEffect(developerTaps) {
|
||||||
if (developerTaps == 0) return@LaunchedEffect
|
if (developerTaps == 0) return@LaunchedEffect
|
||||||
if (showDeveloperSettings) {
|
if (showDeveloperSettings) {
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_already_enabled))
|
snackbarHostState.showSnackbar(resources.getString(R.string.developer_options_already_enabled))
|
||||||
developerTaps = 0
|
developerTaps = 0
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
@@ -161,7 +164,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(
|
||||||
context.getString(
|
resources.getString(
|
||||||
R.string.developer_options_taps,
|
R.string.developer_options_taps,
|
||||||
remaining
|
remaining
|
||||||
),
|
),
|
||||||
@@ -169,7 +172,7 @@ fun AboutSettingsScreen(
|
|||||||
)
|
)
|
||||||
} else if (remaining == 0) {
|
} else if (remaining == 0) {
|
||||||
viewModel.showDeveloperSettings.update(true)
|
viewModel.showDeveloperSettings.update(true)
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_enabled))
|
snackbarHostState.showSnackbar(resources.getString(R.string.developer_options_enabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the counter
|
// Reset the counter
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ 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
|
||||||
@@ -64,9 +65,10 @@ fun AdvancedSettingsScreen(
|
|||||||
viewModel: AdvancedSettingsViewModel = koinViewModel()
|
viewModel: AdvancedSettingsViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val memoryLimit = remember {
|
val resources = LocalResources.current
|
||||||
|
val memoryLimit = remember(resources) {
|
||||||
val activityManager = context.getSystemService<ActivityManager>()!!
|
val activityManager = context.getSystemService<ActivityManager>()!!
|
||||||
context.getString(
|
resources.getString(
|
||||||
R.string.device_memory_limit_format,
|
R.string.device_memory_limit_format,
|
||||||
activityManager.memoryClass,
|
activityManager.memoryClass,
|
||||||
activityManager.largeMemoryClass
|
activityManager.largeMemoryClass
|
||||||
@@ -183,7 +185,7 @@ fun AdvancedSettingsScreen(
|
|||||||
ClipData.newPlainText("Device Information", deviceContent)
|
ClipData.newPlainText("Device Information", deviceContent)
|
||||||
)
|
)
|
||||||
|
|
||||||
context.toast(context.getString(R.string.toast_copied_to_clipboard))
|
context.toast(resources.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),
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
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.network.downloader.DownloaderPackageState
|
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||||
import app.revanced.manager.ui.component.AppLabel
|
import app.revanced.manager.ui.component.AppLabel
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.ConfirmDialog
|
import app.revanced.manager.ui.component.ConfirmDialog
|
||||||
@@ -60,7 +60,7 @@ fun DownloadsSettingsScreen(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
||||||
val downloaderStates by viewModel.downloaderStates.collectAsStateWithLifecycle()
|
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -68,8 +68,8 @@ fun DownloadsSettingsScreen(
|
|||||||
ConfirmDialog(
|
ConfirmDialog(
|
||||||
onDismiss = { showDeleteConfirmationDialog = false },
|
onDismiss = { showDeleteConfirmationDialog = false },
|
||||||
onConfirm = { viewModel.deleteApps() },
|
onConfirm = { viewModel.deleteApps() },
|
||||||
title = stringResource(R.string.downloader_delete_apps_title),
|
title = stringResource(R.string.downloader_plugin_delete_apps_title),
|
||||||
description = stringResource(R.string.downloader_delete_apps_description),
|
description = stringResource(R.string.downloader_plugin_delete_apps_description),
|
||||||
icon = Icons.Outlined.Delete
|
icon = Icons.Outlined.Delete
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -92,118 +92,111 @@ fun DownloadsSettingsScreen(
|
|||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
onRefresh = viewModel::refreshDownloader,
|
onRefresh = viewModel::refreshPlugins,
|
||||||
isRefreshing = viewModel.isRefreshingDownloader,
|
isRefreshing = viewModel.isRefreshingPlugins,
|
||||||
modifier = Modifier.padding(paddingValues)
|
modifier = Modifier.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
LazyColumnWithScrollbar(
|
LazyColumnWithScrollbar(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
GroupHeader(stringResource(R.string.downloader))
|
GroupHeader(stringResource(R.string.downloader_plugins))
|
||||||
}
|
}
|
||||||
|
pluginStates.forEach { (packageName, state) ->
|
||||||
if (downloaderStates.isNotEmpty()) {
|
item(key = packageName) {
|
||||||
downloaderStates.forEach { (packageName, state) ->
|
var showDialog by rememberSaveable {
|
||||||
item(key = packageName) {
|
mutableStateOf(false)
|
||||||
var showDialog by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dismiss() {
|
|
||||||
showDialog = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val packageInfo =
|
|
||||||
remember(packageName) {
|
|
||||||
viewModel.pm.getPackageInfo(
|
|
||||||
packageName
|
|
||||||
)
|
|
||||||
} ?: return@item
|
|
||||||
|
|
||||||
if (showDialog) {
|
|
||||||
val signature =
|
|
||||||
remember(packageName) {
|
|
||||||
val androidSignature =
|
|
||||||
viewModel.pm.getSignature(packageName)
|
|
||||||
val hash = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(androidSignature.toByteArray())
|
|
||||||
hash.toHexString(format = HexFormat.UpperCase)
|
|
||||||
}
|
|
||||||
val appName = remember {
|
|
||||||
packageInfo.applicationInfo?.loadLabel(context.packageManager)
|
|
||||||
?.toString()
|
|
||||||
?: packageName
|
|
||||||
}
|
|
||||||
|
|
||||||
when (state) {
|
|
||||||
is DownloaderPackageState.Loaded -> TrustDialog(
|
|
||||||
title = R.string.downloader_revoke_trust_dialog_title,
|
|
||||||
body = stringResource(
|
|
||||||
R.string.downloader_trust_dialog_body,
|
|
||||||
packageName,
|
|
||||||
signature
|
|
||||||
),
|
|
||||||
downloaderName = appName,
|
|
||||||
signature = signature,
|
|
||||||
onDismiss = ::dismiss,
|
|
||||||
onConfirm = {
|
|
||||||
viewModel.revokeDownloaderTrust(packageName)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
is DownloaderPackageState.Failed -> ExceptionViewerDialog(
|
|
||||||
text = remember(state.throwable) {
|
|
||||||
state.throwable.stackTraceToString()
|
|
||||||
},
|
|
||||||
onDismiss = ::dismiss
|
|
||||||
)
|
|
||||||
|
|
||||||
is DownloaderPackageState.Untrusted -> TrustDialog(
|
|
||||||
title = R.string.downloader_trust_dialog_title,
|
|
||||||
body = stringResource(
|
|
||||||
R.string.downloader_trust_dialog_body
|
|
||||||
),
|
|
||||||
downloaderName = appName,
|
|
||||||
signature = signature,
|
|
||||||
onDismiss = ::dismiss,
|
|
||||||
onConfirm = {
|
|
||||||
viewModel.trustDownloader(packageName)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsListItem(
|
|
||||||
modifier = Modifier.clickable { showDialog = true },
|
|
||||||
headlineContent = {
|
|
||||||
AppLabel(
|
|
||||||
packageInfo = packageInfo,
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
},
|
|
||||||
supportingContent = when (state) {
|
|
||||||
is DownloaderPackageState.Loaded -> {
|
|
||||||
val names = state.downloader.joinToString("\n") { it.name }
|
|
||||||
if (names.isNotEmpty())
|
|
||||||
stringResource(R.string.downloader_state_trusted, "\n\n$names")
|
|
||||||
else
|
|
||||||
stringResource(R.string.downloader_state_trusted)
|
|
||||||
}
|
|
||||||
|
|
||||||
is DownloaderPackageState.Failed -> stringResource(R.string.downloader_state_failed)
|
|
||||||
is DownloaderPackageState.Untrusted -> stringResource(R.string.downloader_state_untrusted)
|
|
||||||
},
|
|
||||||
trailingContent = { Text(packageInfo.versionName!!) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun dismiss() {
|
||||||
|
showDialog = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val packageInfo =
|
||||||
|
remember(packageName) {
|
||||||
|
viewModel.pm.getPackageInfo(
|
||||||
|
packageName
|
||||||
|
)
|
||||||
|
} ?: return@item
|
||||||
|
|
||||||
|
if (showDialog) {
|
||||||
|
val signature =
|
||||||
|
remember(packageName) {
|
||||||
|
val androidSignature =
|
||||||
|
viewModel.pm.getSignature(packageName)
|
||||||
|
val hash = MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(androidSignature.toByteArray())
|
||||||
|
hash.toHexString(format = HexFormat.UpperCase)
|
||||||
|
}
|
||||||
|
val appName = remember {
|
||||||
|
packageInfo.applicationInfo?.loadLabel(context.packageManager)
|
||||||
|
?.toString()
|
||||||
|
?: packageName
|
||||||
|
}
|
||||||
|
|
||||||
|
when (state) {
|
||||||
|
is DownloaderPluginState.Loaded -> TrustDialog(
|
||||||
|
title = R.string.downloader_plugin_revoke_trust_dialog_title,
|
||||||
|
body = stringResource(
|
||||||
|
R.string.downloader_plugin_trust_dialog_body,
|
||||||
|
packageName,
|
||||||
|
signature
|
||||||
|
),
|
||||||
|
pluginName = appName,
|
||||||
|
signature = signature,
|
||||||
|
onDismiss = ::dismiss,
|
||||||
|
onConfirm = {
|
||||||
|
viewModel.revokePluginTrust(packageName)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
is DownloaderPluginState.Failed -> ExceptionViewerDialog(
|
||||||
|
text = remember(state.throwable) {
|
||||||
|
state.throwable.stackTraceToString()
|
||||||
|
},
|
||||||
|
onDismiss = ::dismiss
|
||||||
|
)
|
||||||
|
|
||||||
|
is DownloaderPluginState.Untrusted -> TrustDialog(
|
||||||
|
title = R.string.downloader_plugin_trust_dialog_title,
|
||||||
|
body = stringResource(
|
||||||
|
R.string.downloader_plugin_trust_dialog_body
|
||||||
|
),
|
||||||
|
pluginName = appName,
|
||||||
|
signature = signature,
|
||||||
|
onDismiss = ::dismiss,
|
||||||
|
onConfirm = {
|
||||||
|
viewModel.trustPlugin(packageName)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsListItem(
|
||||||
|
modifier = Modifier.clickable { showDialog = true },
|
||||||
|
headlineContent = {
|
||||||
|
AppLabel(
|
||||||
|
packageInfo = packageInfo,
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = stringResource(
|
||||||
|
when (state) {
|
||||||
|
is DownloaderPluginState.Loaded -> R.string.downloader_plugin_state_trusted
|
||||||
|
is DownloaderPluginState.Failed -> R.string.downloader_plugin_state_failed
|
||||||
|
is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted
|
||||||
|
}
|
||||||
|
),
|
||||||
|
trailingContent = { Text(packageInfo.versionName!!) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
if (pluginStates.isEmpty()) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.no_downloader_installed),
|
stringResource(R.string.downloader_no_plugins_installed),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
@@ -247,7 +240,7 @@ fun DownloadsSettingsScreen(
|
|||||||
private fun TrustDialog(
|
private fun TrustDialog(
|
||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
body: String,
|
body: String,
|
||||||
downloaderName: String,
|
pluginName: String,
|
||||||
signature: String,
|
signature: String,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onConfirm: () -> Unit
|
onConfirm: () -> Unit
|
||||||
@@ -275,8 +268,8 @@ private fun TrustDialog(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.downloader_trust_dialog_name,
|
R.string.downloader_plugin_trust_dialog_plugin,
|
||||||
downloaderName
|
pluginName
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
@@ -286,7 +279,7 @@ private fun TrustDialog(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.downloader_trust_dialog_signature,
|
R.string.downloader_plugin_trust_dialog_signature,
|
||||||
signature.chunked(2).joinToString(" ")
|
signature.chunked(2).joinToString(" ")
|
||||||
), modifier = Modifier.padding(12.dp)
|
), modifier = Modifier.padding(12.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ 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
|
||||||
@@ -19,6 +21,7 @@ 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
|
||||||
@@ -38,6 +41,7 @@ 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
|
||||||
@@ -48,6 +52,7 @@ 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(
|
||||||
@@ -55,6 +60,17 @@ 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(
|
||||||
@@ -74,6 +90,24 @@ 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 },
|
||||||
@@ -157,3 +191,63 @@ 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,6 +36,7 @@ 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
|
||||||
@@ -64,6 +65,7 @@ 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) }
|
||||||
|
|
||||||
@@ -108,7 +110,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(context.getString(R.string.import_keystore_wrong_credentials))
|
if (!result) context.toast(resources.getString(R.string.import_keystore_wrong_credentials))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,7 +168,7 @@ fun ImportExportSettingsScreen(
|
|||||||
GroupItem(
|
GroupItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!vm.canExport()) {
|
if (!vm.canExport()) {
|
||||||
context.toast(context.getString(R.string.export_keystore_unavailable))
|
context.toast(resources.getString(R.string.export_keystore_unavailable))
|
||||||
return@GroupItem
|
return@GroupItem
|
||||||
}
|
}
|
||||||
exportKeystoreLauncher.launch("Manager.keystore")
|
exportKeystoreLauncher.launch("Manager.keystore")
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ 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.libraryColors
|
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||||
|
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
|
||||||
@@ -33,16 +36,23 @@ 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(
|
||||||
backgroundColor = MaterialTheme.colorScheme.background,
|
libraryBackgroundColor = MaterialTheme.colorScheme.background,
|
||||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
libraryContentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
badgeBackgroundColor = MaterialTheme.colorScheme.primary,
|
versionChipColors = chipColors,
|
||||||
badgeContentColor = MaterialTheme.colorScheme.onPrimary,
|
licenseChipColors = chipColors,
|
||||||
|
fundingChipColors = chipColors,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Scrollbar(lazyListState = lazyListState, modifier = Modifier.padding(paddingValues))
|
Scrollbar(lazyListState = lazyListState, modifier = Modifier.padding(paddingValues))
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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
|
||||||
@@ -33,6 +34,7 @@ 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())
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ fun UpdatesSettingsScreen(
|
|||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
if (!vm.isConnected) {
|
if (!vm.isConnected) {
|
||||||
context.toast(context.getString(R.string.no_network_toast))
|
context.toast(resources.getString(R.string.no_network_toast))
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
if (vm.checkForUpdates()) onUpdateClick()
|
if (vm.checkForUpdates()) onUpdateClick()
|
||||||
@@ -70,7 +72,7 @@ fun UpdatesSettingsScreen(
|
|||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
if (!vm.isConnected) {
|
if (!vm.isConnected) {
|
||||||
context.toast(context.getString(R.string.no_network_toast))
|
context.toast(resources.getString(R.string.no_network_toast))
|
||||||
return@clickable
|
return@clickable
|
||||||
}
|
}
|
||||||
onChangelogClick()
|
onChangelogClick()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
@@ -14,12 +15,15 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
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.domain.bundles.PatchBundleSource
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||||
|
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.DownloaderRepository
|
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.network.api.ReVancedAPI
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
@@ -30,7 +34,7 @@ import kotlinx.coroutines.launch
|
|||||||
class DashboardViewModel(
|
class DashboardViewModel(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val patchBundleRepository: PatchBundleRepository,
|
private val patchBundleRepository: PatchBundleRepository,
|
||||||
private val downloaderRepository: DownloaderRepository,
|
private val downloaderPluginRepository: DownloaderPluginRepository,
|
||||||
private val reVancedAPI: ReVancedAPI,
|
private val reVancedAPI: ReVancedAPI,
|
||||||
private val networkInfo: NetworkInfo,
|
private val networkInfo: NetworkInfo,
|
||||||
val prefs: PreferencesManager,
|
val prefs: PreferencesManager,
|
||||||
@@ -41,8 +45,8 @@ class DashboardViewModel(
|
|||||||
private val contentResolver: ContentResolver = app.contentResolver
|
private val contentResolver: ContentResolver = app.contentResolver
|
||||||
private val powerManager = app.getSystemService<PowerManager>()!!
|
private val powerManager = app.getSystemService<PowerManager>()!!
|
||||||
|
|
||||||
val newDownloaderAvailable =
|
val newDownloaderPluginsAvailable =
|
||||||
downloaderRepository.newDownloaderPackageNames.map { it.isNotEmpty() }
|
downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen.
|
* Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen.
|
||||||
@@ -67,8 +71,8 @@ class DashboardViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ignoreNewDownloader() = viewModelScope.launch {
|
fun ignoreNewDownloaderPlugins() = viewModelScope.launch {
|
||||||
downloaderRepository.acknowledgeAllNewDownloader()
|
downloaderPluginRepository.acknowledgeAllNewPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkForManagerUpdates() {
|
private suspend fun checkForManagerUpdates() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
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
|
||||||
@@ -7,7 +8,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
||||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||||
import app.revanced.manager.domain.repository.DownloaderRepository
|
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.mutableStateSetOf
|
import app.revanced.manager.util.mutableStateSetOf
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -18,10 +19,10 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
class DownloadsViewModel(
|
class DownloadsViewModel(
|
||||||
private val downloadedAppRepository: DownloadedAppRepository,
|
private val downloadedAppRepository: DownloadedAppRepository,
|
||||||
private val downloaderRepository: DownloaderRepository,
|
private val downloaderPluginRepository: DownloaderPluginRepository,
|
||||||
val pm: PM
|
val pm: PM
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val downloaderStates = downloaderRepository.downloaderPackageStates
|
val downloaderPluginStates = downloaderPluginRepository.pluginStates
|
||||||
val downloadedApps = downloadedAppRepository.getAll().map { downloadedApps ->
|
val downloadedApps = downloadedAppRepository.getAll().map { downloadedApps ->
|
||||||
downloadedApps.sortedWith(
|
downloadedApps.sortedWith(
|
||||||
compareBy<DownloadedApp> {
|
compareBy<DownloadedApp> {
|
||||||
@@ -31,7 +32,7 @@ class DownloadsViewModel(
|
|||||||
}
|
}
|
||||||
val appSelection = mutableStateSetOf<DownloadedApp>()
|
val appSelection = mutableStateSetOf<DownloadedApp>()
|
||||||
|
|
||||||
var isRefreshingDownloader by mutableStateOf(false)
|
var isRefreshingPlugins by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun toggleApp(downloadedApp: DownloadedApp) {
|
fun toggleApp(downloadedApp: DownloadedApp) {
|
||||||
@@ -51,17 +52,17 @@ class DownloadsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshDownloader() = viewModelScope.launch {
|
fun refreshPlugins() = viewModelScope.launch {
|
||||||
isRefreshingDownloader = true
|
isRefreshingPlugins = true
|
||||||
downloaderRepository.reload()
|
downloaderPluginRepository.reload()
|
||||||
isRefreshingDownloader = false
|
isRefreshingPlugins = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trustDownloader(packageName: String) = viewModelScope.launch {
|
fun trustPlugin(packageName: String) = viewModelScope.launch {
|
||||||
downloaderRepository.trustPackage(packageName)
|
downloaderPluginRepository.trustPackage(packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun revokeDownloaderTrust(packageName: String) = viewModelScope.launch {
|
fun revokePluginTrust(packageName: String) = viewModelScope.launch {
|
||||||
downloaderRepository.revokeTrustForPackage(packageName)
|
downloaderPluginRepository.revokeTrustForPackage(packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,26 @@
|
|||||||
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(
|
||||||
@StringRes val titleResId: Int,
|
@param:StringRes val titleResId: Int,
|
||||||
@StringRes val descriptionResId: Int,
|
@param:StringRes val descriptionResId: Int,
|
||||||
val onConfirm: () -> Unit,
|
val onConfirm: () -> Unit,
|
||||||
val dialogOptionName: String? = null
|
val dialogOptionName: String? = null
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import app.revanced.manager.patcher.StepId
|
|||||||
import app.revanced.manager.patcher.logger.LogLevel
|
import app.revanced.manager.patcher.logger.LogLevel
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||||
import app.revanced.manager.downloader.DownloaderHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.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.SelectedApp
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.ui.model.State
|
||||||
@@ -79,7 +79,7 @@ import java.io.File
|
|||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
@OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||||
class PatcherViewModel(
|
class PatcherViewModel(
|
||||||
private val input: Patcher.ViewModelParams
|
private val input: Patcher.ViewModelParams
|
||||||
) : ViewModel(), KoinComponent, InstallerModel {
|
) : ViewModel(), KoinComponent, InstallerModel {
|
||||||
@@ -184,13 +184,13 @@ class PatcherViewModel(
|
|||||||
input.options,
|
input.options,
|
||||||
logger,
|
logger,
|
||||||
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
||||||
handleStartActivityRequest = { downloader, intent ->
|
handleStartActivityRequest = { plugin, intent ->
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
||||||
try {
|
try {
|
||||||
// Wait for the dialog interaction.
|
// Wait for the dialog interaction.
|
||||||
val accepted = with(CompletableDeferred<Boolean>()) {
|
val accepted = with(CompletableDeferred<Boolean>()) {
|
||||||
currentActivityRequest = this to downloader.name
|
currentActivityRequest = this to plugin.name
|
||||||
|
|
||||||
await()
|
await()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,19 +22,19 @@ import app.revanced.manager.R
|
|||||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||||
import app.revanced.manager.domain.installer.RootInstaller
|
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.DownloaderRepository
|
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.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloader
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
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.downloader.GetScope
|
import app.revanced.manager.plugin.downloader.GetScope
|
||||||
import app.revanced.manager.downloader.DownloaderHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.downloader.UserInteractionException
|
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
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.SelectedApplicationInfo
|
||||||
@@ -59,7 +59,7 @@ 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
|
||||||
|
|
||||||
@OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||||
class SelectedAppInfoViewModel(
|
class SelectedAppInfoViewModel(
|
||||||
input: SelectedApplicationInfo.ViewModelParams
|
input: SelectedApplicationInfo.ViewModelParams
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
@@ -67,13 +67,13 @@ class SelectedAppInfoViewModel(
|
|||||||
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 downloaderRepository: DownloaderRepository = get()
|
private val pluginsRepository: DownloaderPluginRepository = get()
|
||||||
private val installedAppRepository: InstalledAppRepository = get()
|
private val installedAppRepository: InstalledAppRepository = get()
|
||||||
private val rootInstaller: RootInstaller = get()
|
private val rootInstaller: RootInstaller = 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()
|
val prefs: PreferencesManager = get()
|
||||||
val downloader = downloaderRepository.loadedDownloaderPackageFlow
|
val plugins = pluginsRepository.loadedPluginsFlow
|
||||||
val desiredVersion = input.app.version
|
val desiredVersion = input.app.version
|
||||||
val packageName = input.app.packageName
|
val packageName = input.app.packageName
|
||||||
|
|
||||||
@@ -160,15 +160,15 @@ class SelectedAppInfoViewModel(
|
|||||||
|
|
||||||
var showSourceSelector by mutableStateOf(false)
|
var showSourceSelector by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
private var downloaderAction: Pair<LoadedDownloader, Job>? by mutableStateOf(null)
|
private var pluginAction: Pair<LoadedDownloaderPlugin, Job>? by mutableStateOf(null)
|
||||||
val activeDownloaderAction get() = downloaderAction?.first?.packageName
|
val activePluginAction get() = pluginAction?.first?.packageName
|
||||||
private var launchedActivity by mutableStateOf<CompletableDeferred<ActivityResult>?>(null)
|
private var launchedActivity by mutableStateOf<CompletableDeferred<ActivityResult>?>(null)
|
||||||
private val launchActivityChannel = Channel<Intent>()
|
private val launchActivityChannel = Channel<Intent>()
|
||||||
val launchActivityFlow = launchActivityChannel.receiveAsFlow()
|
val launchActivityFlow = launchActivityChannel.receiveAsFlow()
|
||||||
|
|
||||||
val errorFlow = combine(downloader, snapshotFlow { selectedApp }) { downloaderList, app ->
|
val errorFlow = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app ->
|
||||||
when {
|
when {
|
||||||
app is SelectedApp.Search && downloaderList.isEmpty() -> Error.NoDownloader
|
app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,23 +178,23 @@ class SelectedAppInfoViewModel(
|
|||||||
showSourceSelector = true
|
showSourceSelector = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelDownloaderAction() {
|
private fun cancelPluginAction() {
|
||||||
downloaderAction?.second?.cancel()
|
pluginAction?.second?.cancel()
|
||||||
downloaderAction = null
|
pluginAction = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissSourceSelector() {
|
fun dismissSourceSelector() {
|
||||||
cancelDownloaderAction()
|
cancelPluginAction()
|
||||||
showSourceSelector = false
|
showSourceSelector = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchUsingDownloader(downloader: LoadedDownloader) {
|
fun searchUsingPlugin(plugin: LoadedDownloaderPlugin) {
|
||||||
cancelDownloaderAction()
|
cancelPluginAction()
|
||||||
downloaderAction = downloader to viewModelScope.launch {
|
pluginAction = plugin to viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val scope = object : GetScope {
|
val scope = object : GetScope {
|
||||||
override val hostPackageName = app.packageName
|
override val hostPackageName = app.packageName
|
||||||
override val downloaderPackageName = downloader.packageName
|
override val pluginPackageName = plugin.packageName
|
||||||
override suspend fun requestStartActivity(intent: Intent) =
|
override suspend fun requestStartActivity(intent: Intent) =
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (launchedActivity != null) error("Previous activity has not finished")
|
if (launchedActivity != null) error("Previous activity has not finished")
|
||||||
@@ -219,7 +219,7 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
downloader.get(scope, packageName, desiredVersion)
|
plugin.get(scope, packageName, desiredVersion)
|
||||||
}?.let { (data, version) ->
|
}?.let { (data, version) ->
|
||||||
if (desiredVersion != null && version != desiredVersion) {
|
if (desiredVersion != null && version != desiredVersion) {
|
||||||
app.toast(app.getString(R.string.downloader_invalid_version))
|
app.toast(app.getString(R.string.downloader_invalid_version))
|
||||||
@@ -228,7 +228,7 @@ class SelectedAppInfoViewModel(
|
|||||||
selectedApp = SelectedApp.Download(
|
selectedApp = SelectedApp.Download(
|
||||||
packageName,
|
packageName,
|
||||||
version,
|
version,
|
||||||
ParceledDownloaderData(downloader, data)
|
ParceledDownloaderData(plugin, data)
|
||||||
)
|
)
|
||||||
} ?: app.toast(app.getString(R.string.downloader_app_not_found))
|
} ?: app.toast(app.getString(R.string.downloader_app_not_found))
|
||||||
} catch (e: UserInteractionException.Activity) {
|
} catch (e: UserInteractionException.Activity) {
|
||||||
@@ -239,13 +239,13 @@ class SelectedAppInfoViewModel(
|
|||||||
app.toast(app.getString(R.string.downloader_error, e.simpleMessage()))
|
app.toast(app.getString(R.string.downloader_error, e.simpleMessage()))
|
||||||
Log.e(tag, "Downloader.get threw an exception", e)
|
Log.e(tag, "Downloader.get threw an exception", e)
|
||||||
} finally {
|
} finally {
|
||||||
downloaderAction = null
|
pluginAction = null
|
||||||
dismissSourceSelector()
|
dismissSourceSelector()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleDownloaderActivityResult(result: ActivityResult) {
|
fun handlePluginActivityResult(result: ActivityResult) {
|
||||||
launchedActivity?.complete(result)
|
launchedActivity?.complete(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class Error(@param:StringRes val resourceId: Int) {
|
enum class Error(@param:StringRes val resourceId: Int) {
|
||||||
NoDownloader(R.string.no_downloader_available)
|
NoPlugins(R.string.downloader_no_plugins_available)
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|||||||
@@ -90,8 +90,10 @@ class UpdateViewModel(
|
|||||||
http.download(location) {
|
http.download(location) {
|
||||||
url(release.downloadUrl)
|
url(release.downloadUrl)
|
||||||
onDownload { bytesSentTotal, contentLength ->
|
onDownload { bytesSentTotal, contentLength ->
|
||||||
downloadedSize = bytesSentTotal
|
withContext(Dispatchers.Main) {
|
||||||
totalSize = contentLength
|
downloadedSize = bytesSentTotal
|
||||||
|
contentLength?.let { totalSize = it }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
installUpdate()
|
installUpdate()
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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,12 +15,10 @@ 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.mutableIntStateOf
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.snapshotFlow
|
||||||
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
|
||||||
@@ -43,7 +41,6 @@ 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
|
||||||
@@ -51,9 +48,11 @@ 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?>>>
|
||||||
@@ -169,7 +168,7 @@ fun LocalDateTime.relativeTime(context: Context): String {
|
|||||||
else -> LocalDateTime.Format {
|
else -> LocalDateTime.Format {
|
||||||
monthName(MonthNames.ENGLISH_ABBREVIATED)
|
monthName(MonthNames.ENGLISH_ABBREVIATED)
|
||||||
char(' ')
|
char(' ')
|
||||||
dayOfMonth()
|
day()
|
||||||
if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) {
|
if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) {
|
||||||
chars(", ")
|
chars(", ")
|
||||||
year()
|
year()
|
||||||
@@ -196,7 +195,12 @@ val transparentListItemColors
|
|||||||
.also { transparentListItemColorsCached = it }
|
.also { transparentListItemColorsCached = it }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> EventEffect(flow: Flow<T>, vararg keys: Any?, state: Lifecycle.State = Lifecycle.State.STARTED, block: suspend (T) -> Unit) {
|
fun <T> EventEffect(
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -212,40 +216,36 @@ fun <T> EventEffect(flow: Flow<T>, vararg keys: Any?, state: Lifecycle.State = L
|
|||||||
const val isScrollingUpSensitivity = 10
|
const val isScrollingUpSensitivity = 10
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LazyListState.isScrollingUp(): State<Boolean> {
|
fun LazyListState.isScrollingUp() = produceState(true, this) {
|
||||||
return remember(this) {
|
var previousIndex = firstVisibleItemIndex
|
||||||
var previousIndex by mutableIntStateOf(firstVisibleItemIndex)
|
var previousScrollOffset = firstVisibleItemScrollOffset
|
||||||
var previousScrollOffset by mutableIntStateOf(firstVisibleItemScrollOffset)
|
|
||||||
|
|
||||||
derivedStateOf {
|
snapshotFlow {
|
||||||
val indexChanged = previousIndex != firstVisibleItemIndex
|
firstVisibleItemIndex to firstVisibleItemScrollOffset
|
||||||
val offsetChanged =
|
}.collect { (index, scrollOffset) ->
|
||||||
kotlin.math.abs(previousScrollOffset - firstVisibleItemScrollOffset) > isScrollingUpSensitivity
|
val indexChanged = previousIndex != index
|
||||||
|
val offsetChanged = abs(previousScrollOffset - scrollOffset) > isScrollingUpSensitivity
|
||||||
|
|
||||||
if (indexChanged) {
|
value = when {
|
||||||
previousIndex > firstVisibleItemIndex
|
indexChanged -> previousIndex > index
|
||||||
} else if (offsetChanged) {
|
offsetChanged -> previousScrollOffset > scrollOffset
|
||||||
previousScrollOffset > firstVisibleItemScrollOffset
|
else -> value
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}.also {
|
|
||||||
previousIndex = firstVisibleItemIndex
|
|
||||||
previousScrollOffset = firstVisibleItemScrollOffset
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
previousIndex = index
|
||||||
|
previousScrollOffset = scrollOffset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: support sensitivity
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScrollState.isScrollingUp(): State<Boolean> {
|
fun ScrollState.isScrollingUp() = produceState(true, this) {
|
||||||
return remember(this) {
|
var previousScrollOffset = this@isScrollingUp.value
|
||||||
var previousScrollOffset by mutableIntStateOf(value)
|
|
||||||
derivedStateOf {
|
snapshotFlow { this@isScrollingUp.value }.collect { scrollOffset ->
|
||||||
(previousScrollOffset >= value).also {
|
if (abs(previousScrollOffset - scrollOffset) > isScrollingUpSensitivity) {
|
||||||
previousScrollOffset = value
|
value = previousScrollOffset >= scrollOffset
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previousScrollOffset = scrollOffset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ Second \"item\" text"</string>
|
|||||||
-->
|
-->
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">ReVanced Manager</string>
|
<string name="app_name">ReVanced Manager</string>
|
||||||
<string name="patcher">Patcher</string>
|
<string name="patcher">Patcher test</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>
|
||||||
|
|
||||||
<string name="downloader_host_permission_label">ReVanced Manager downloader host</string>
|
<string name="plugin_host_permission_label">ReVanced Manager plugin host</string>
|
||||||
<string name="downloader_host_permission_description">Used to control access to ReVanced Manager downloader. Only ReVanced Manager has this.</string>
|
<string name="plugin_host_permission_description">Used to control access to ReVanced Manager plugins. Only ReVanced Manager has this.</string>
|
||||||
|
|
||||||
<string name="toast_copied_to_clipboard">Copied!</string>
|
<string name="toast_copied_to_clipboard">Copied!</string>
|
||||||
<string name="copy_to_clipboard">Copy to clipboard</string>
|
<string name="copy_to_clipboard">Copy to clipboard</string>
|
||||||
@@ -30,7 +30,7 @@ Second \"item\" text"</string>
|
|||||||
<string name="select_app">Select an app</string>
|
<string name="select_app">Select an app</string>
|
||||||
<string name="patches_count_selected">%1$d/%2$d selected</string>
|
<string name="patches_count_selected">%1$d/%2$d selected</string>
|
||||||
|
|
||||||
<string name="new_downloader_notification">New downloader available. Click here to configure them.</string>
|
<string name="new_downloader_plugins_notification">New downloader plugins available. Click here to configure them.</string>
|
||||||
<string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string>
|
<string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string>
|
||||||
|
|
||||||
<string name="import_">Import</string>
|
<string name="import_">Import</string>
|
||||||
@@ -56,7 +56,7 @@ Second \"item\" text"</string>
|
|||||||
<string name="app_source_dialog_title">Select source</string>
|
<string name="app_source_dialog_title">Select source</string>
|
||||||
<string name="app_source_dialog_option_auto">Auto</string>
|
<string name="app_source_dialog_option_auto">Auto</string>
|
||||||
<string name="app_source_dialog_option_auto_description">Use all available downloader to download the app</string>
|
<string name="app_source_dialog_option_auto_description">Use all available downloader to download the app</string>
|
||||||
<string name="app_source_dialog_option_auto_unavailable">No downloader available</string>
|
<string name="app_source_dialog_option_auto_unavailable">No plugins available</string>
|
||||||
<string name="app_source_dialog_option_installed_no_root">Mounted apps cannot be patched again without root access</string>
|
<string name="app_source_dialog_option_installed_no_root">Mounted apps cannot be patched again without root access</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>
|
||||||
|
|
||||||
@@ -83,11 +83,11 @@ 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">Theme, dynamic color</string>
|
<string name="general_description">Language, 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>
|
||||||
<string name="downloads_description">Downloader and downloaded apps</string>
|
<string name="downloads_description">Downloader plugins and downloaded apps</string>
|
||||||
<string name="import_export">Import & export</string>
|
<string name="import_export">Import & export</string>
|
||||||
<string name="import_export_description">Keystore, patch options and selection</string>
|
<string name="import_export_description">Keystore, patch options and selection</string>
|
||||||
<string name="advanced">Advanced</string>
|
<string name="advanced">Advanced</string>
|
||||||
@@ -104,6 +104,9 @@ 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>
|
||||||
@@ -175,17 +178,17 @@ You will not be able to update the previously installed apps from this source."<
|
|||||||
<string name="patch_options_reset_all">Reset patch options globally</string>
|
<string name="patch_options_reset_all">Reset patch options globally</string>
|
||||||
<string name="patch_options_reset_all_dialog_description">You are about to reset all patch options. You will have to reapply each option again.</string>
|
<string name="patch_options_reset_all_dialog_description">You are about to reset all patch options. You will have to reapply each option again.</string>
|
||||||
<string name="patch_options_reset_all_description">Resets all patch options</string>
|
<string name="patch_options_reset_all_description">Resets all patch options</string>
|
||||||
<string name="downloader">Downloader</string>
|
<string name="downloader_plugins">Plugins</string>
|
||||||
<string name="downloader_state_trusted">Trusted%s</string>
|
<string name="downloader_plugin_state_trusted">Trusted</string>
|
||||||
<string name="downloader_state_failed">Failed to load. Click for more details</string>
|
<string name="downloader_plugin_state_failed">Failed to load. Click for more details</string>
|
||||||
<string name="downloader_state_untrusted">Untrusted</string>
|
<string name="downloader_plugin_state_untrusted">Untrusted</string>
|
||||||
<string name="downloader_trust_dialog_title">Trust downloader?</string>
|
<string name="downloader_plugin_trust_dialog_title">Trust plugin?</string>
|
||||||
<string name="downloader_revoke_trust_dialog_title">Revoke trust?</string>
|
<string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string>
|
||||||
<string name="downloader_trust_dialog_body">Continuing will allow this downloader to run on your system.\n\nOnly enable this downloader if you trust it. Downloader can execute arbitrary code and may compromise your device.</string>
|
<string name="downloader_plugin_trust_dialog_body">Continuing will allow this plugin to run on your system.\n\nOnly enable this plugin if you trust it. Plugins can execute arbitrary code and may compromise your device.</string>
|
||||||
<string name="downloader_trust_dialog_signature">Signature:\n\n%s</string>
|
<string name="downloader_plugin_trust_dialog_signature">Signature:\n\n%s</string>
|
||||||
<string name="downloader_trust_dialog_name">Downloader:\n%s</string>
|
<string name="downloader_plugin_trust_dialog_plugin">Plugin:\n%s</string>
|
||||||
<string name="downloader_delete_apps_title">Delete selected apps</string>
|
<string name="downloader_plugin_delete_apps_title">Delete selected apps</string>
|
||||||
<string name="downloader_delete_apps_description">Are you sure you want to delete the selected apps?</string>
|
<string name="downloader_plugin_delete_apps_description">Are you sure you want to delete the selected apps?</string>
|
||||||
<string name="downloader_settings_no_apps">No downloaded apps found.</string>
|
<string name="downloader_settings_no_apps">No downloaded apps found.</string>
|
||||||
|
|
||||||
<string name="search_apps">Search apps…</string>
|
<string name="search_apps">Search apps…</string>
|
||||||
@@ -316,8 +319,8 @@ It is only compatible with the following version(s): %2$s"</string>
|
|||||||
<string name="downloader_invalid_version">Downloader did not fetch the correct version</string>
|
<string name="downloader_invalid_version">Downloader did not fetch the correct version</string>
|
||||||
<string name="downloader_app_not_found">Downloader did not find the app</string>
|
<string name="downloader_app_not_found">Downloader did not find the app</string>
|
||||||
<string name="downloader_error">Downloader error: %s</string>
|
<string name="downloader_error">Downloader error: %s</string>
|
||||||
<string name="no_downloader_installed">No downloader installed.</string>
|
<string name="downloader_no_plugins_installed">No downloader installed.</string>
|
||||||
<string name="no_downloader_available">There are downloader installed but none are trusted. Check your settings.</string>
|
<string name="downloader_no_plugins_available">There are downloaders installed but none are trusted. Check your settings.</string>
|
||||||
<string name="already_patched">Already patched</string>
|
<string name="already_patched">Already patched</string>
|
||||||
|
|
||||||
<string name="patch_selector_sheet_filter_title">Filter</string>
|
<string name="patch_selector_sheet_filter_title">Filter</string>
|
||||||
@@ -345,7 +348,7 @@ It is only compatible with the following version(s): %2$s"</string>
|
|||||||
<string name="save_apk_success">APK Saved</string>
|
<string name="save_apk_success">APK Saved</string>
|
||||||
<string name="sign_fail">Failed to sign APK: %s</string>
|
<string name="sign_fail">Failed to sign APK: %s</string>
|
||||||
<string name="save_logs">Save logs</string>
|
<string name="save_logs">Save logs</string>
|
||||||
<string name="downloader_activity_dialog_body">User interaction is required in order to proceed with this downloader.</string>
|
<string name="plugin_activity_dialog_body">User interaction is required in order to proceed with this plugin.</string>
|
||||||
<string name="select_install_type">Select installation type</string>
|
<string name="select_install_type">Select installation type</string>
|
||||||
|
|
||||||
<string name="patcher_step_group_preparing">Preparing</string>
|
<string name="patcher_step_group_preparing">Preparing</string>
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
9
crowdin.yml
Normal file
9
crowdin.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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.16.0"
|
ktx = "1.17.0"
|
||||||
material3 = "1.3.2"
|
material3 = "1.4.0"
|
||||||
ui-tooling = "1.8.1"
|
ui-tooling = "1.10.0"
|
||||||
viewmodel-lifecycle = "2.9.0"
|
viewmodel-lifecycle = "2.10.0"
|
||||||
splash-screen = "1.0.1"
|
splash-screen = "1.2.0"
|
||||||
activity = "1.10.1"
|
activity = "1.12.2"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.1"
|
||||||
preferences-datastore = "1.1.2"
|
preferences-datastore = "1.2.0"
|
||||||
work-runtime = "2.10.1"
|
work-runtime = "2.11.0"
|
||||||
compose-bom = "2025.05.00"
|
compose-bom = "2025.12.01"
|
||||||
navigation = "2.8.6"
|
navigation = "2.9.6"
|
||||||
accompanist = "0.37.0"
|
accompanist = "0.37.3"
|
||||||
placeholder = "1.1.2"
|
placeholder = "1.0.12"
|
||||||
reorderable = "2.4.3"
|
reorderable = "3.0.0"
|
||||||
serialization = "1.8.0"
|
serialization = "1.9.0"
|
||||||
collection = "0.3.8"
|
collection = "0.4.0"
|
||||||
datetime = "0.6.1"
|
datetime = "0.7.1"
|
||||||
room-version = "2.7.1"
|
room-version = "2.8.4"
|
||||||
revanced-patcher = "21.0.0"
|
revanced-patcher = "21.0.0"
|
||||||
revanced-library = "3.0.2"
|
revanced-library = "3.0.2"
|
||||||
koin = "3.5.3"
|
koin = "4.1.1"
|
||||||
ktor = "2.3.9"
|
ktor = "3.3.3"
|
||||||
markdown-renderer = "0.30.0"
|
markdown-renderer = "0.39.0"
|
||||||
fading-edges = "1.0.4"
|
fading-edges = "1.0.4"
|
||||||
kotlin = "2.1.10"
|
kotlin = "2.3.0"
|
||||||
android-gradle-plugin = "8.9.1"
|
android-gradle-plugin = "8.13.2"
|
||||||
dev-tools-gradle-plugin = "2.1.10-1.0.29"
|
dev-tools-gradle-plugin = "2.3.4"
|
||||||
about-libraries-gradle-plugin = "12.1.2"
|
about-libraries = "13.2.1"
|
||||||
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.3.3"
|
hidden-api-stub = "4.4.0"
|
||||||
binary-compatibility-validator = "0.17.0"
|
binary-compatibility-validator = "0.18.1"
|
||||||
semver-parser = "3.0.0"
|
semver-parser = "3.0.0"
|
||||||
ackpine = "0.18.5"
|
ackpine = "0.19.1"
|
||||||
|
|
||||||
[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 = "io.github.fornewid", name = "placeholder-material3", version.ref = "placeholder"}
|
placeholder-material3 = { group = "com.eygraber", name = "compose-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,7 +91,8 @@ 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 = { group = "com.mikepenz", name = "aboutlibraries-compose", version.ref = "about-libraries-gradle-plugin" }
|
about-libraries-core = { group = "com.mikepenz", name = "aboutlibraries-compose-core", version.ref = "about-libraries" }
|
||||||
|
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" }
|
||||||
@@ -146,5 +147,6 @@ 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-gradle-plugin" }
|
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries" }
|
||||||
|
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