mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-29 12:50:12 +02:00
Merge branch 'compose-dev' into docs/readme
This commit is contained in:
commit
3c56db4121
61
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal file
61
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
name: 🐞 Bug report
|
||||||
|
description: Create a new bug report.
|
||||||
|
title: 'bug: <title>'
|
||||||
|
labels: [bug]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# ReVanced Manager bug report
|
||||||
|
|
||||||
|
Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Bug description
|
||||||
|
description: |
|
||||||
|
- Describe your bug in detail
|
||||||
|
- Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...)
|
||||||
|
- Add images and videos if possible
|
||||||
|
- List selected patches if applicable
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Version of ReVanced Manager and version & name of application you tried to patch
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Installation type
|
||||||
|
options:
|
||||||
|
- Non-root
|
||||||
|
- Root
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Device logs
|
||||||
|
description: Export logs in ReVanced Manager settings.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Patcher logs
|
||||||
|
description: Export logs in "Patcher" screen.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Your issue will be closed if you don't follow the checklist below!
|
||||||
|
options:
|
||||||
|
- label: This request is not a duplicate of an existing issue.
|
||||||
|
required: true
|
||||||
|
- label: I have chosen an appropriate title.
|
||||||
|
required: true
|
||||||
|
- label: All requested information has been provided properly.
|
||||||
|
required: true
|
||||||
|
- label: The issue is solely related to the ReVanced Manager
|
||||||
|
required: true
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
42
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
Normal file
42
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
name: ⭐ Feature request
|
||||||
|
description: Create a new feature request.
|
||||||
|
title: 'feat: <title>'
|
||||||
|
labels: [feature request]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# ReVanced Manager feature request
|
||||||
|
|
||||||
|
Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Feature description
|
||||||
|
description: Describe your feature in detail.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Motivation
|
||||||
|
description: Explain why the lack of it is a problem.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: In case there is something else you want to add.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Your issue will be closed if you don't follow the checklist below!
|
||||||
|
options:
|
||||||
|
- label: This request is not a duplicate of an existing issue.
|
||||||
|
required: true
|
||||||
|
- label: I have chosen an appropriate title.
|
||||||
|
required: true
|
||||||
|
- label: All requested information has been provided properly.
|
||||||
|
required: true
|
||||||
|
- label: The issue is solely related to the ReVanced Manager
|
||||||
|
required: true
|
15
.github/workflows/pr-build.yml
vendored
15
.github/workflows/pr-build.yml
vendored
@ -17,17 +17,14 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up cache
|
- name: Set up JDK 17
|
||||||
uses: actions/cache@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
java-version: '17'
|
||||||
${{ runner.home }}/.gradle/caches
|
distribution: 'temurin'
|
||||||
${{ runner.home }}/.gradle/wrapper
|
|
||||||
.gradle
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
|
|
||||||
- name: Set up Java
|
- name: Setup Gradle
|
||||||
run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
|
uses: gradle/gradle-build-action@v2
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
env:
|
env:
|
||||||
|
12
.github/workflows/release-build.yml
vendored
12
.github/workflows/release-build.yml
vendored
@ -14,8 +14,16 @@ jobs:
|
|||||||
- name: Set env
|
- name: Set env
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Set up Java
|
- name: Set up JDK 17
|
||||||
run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/gradle-build-action@v2
|
||||||
|
with:
|
||||||
|
cache-disabled: true
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
env:
|
env:
|
||||||
|
674
LICENSE
Normal file
674
LICENSE
Normal file
@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
@ -9,13 +9,13 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.manager"
|
namespace = "app.revanced.manager"
|
||||||
compileSdk = 33
|
compileSdk = 34
|
||||||
buildToolsVersion = "33.0.2"
|
buildToolsVersion = "34.0.0"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "app.revanced.manager"
|
applicationId = "app.revanced.manager"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 33
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "0.0.1"
|
versionName = "0.0.1"
|
||||||
resourceConfigurations.addAll(listOf(
|
resourceConfigurations.addAll(listOf(
|
||||||
@ -54,6 +54,7 @@ android {
|
|||||||
includeInApk = false
|
includeInApk = false
|
||||||
includeInBundle = false
|
includeInBundle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
resources.excludes.addAll(listOf(
|
resources.excludes.addAll(listOf(
|
||||||
"/prebuilt/**",
|
"/prebuilt/**",
|
||||||
@ -156,6 +157,9 @@ dependencies {
|
|||||||
implementation(libs.ktor.content.negotiation)
|
implementation(libs.ktor.content.negotiation)
|
||||||
implementation(libs.ktor.serialization)
|
implementation(libs.ktor.serialization)
|
||||||
|
|
||||||
// Markdown to HTML
|
// Markdown
|
||||||
implementation(libs.markdown)
|
implementation(libs.markdown.renderer)
|
||||||
|
|
||||||
|
// Fading Edges
|
||||||
|
implementation(libs.fading.edges)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "5515d164bc8f713201506d42a02d337f",
|
"identityHash": "802fa2fda94b930bf0ebb85d195f1022",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "patch_bundles",
|
"tableName": "patch_bundles",
|
||||||
@ -160,7 +160,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "downloaded_app",
|
"tableName": "downloaded_app",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `file` TEXT NOT NULL, PRIMARY KEY(`package_name`, `version`))",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, PRIMARY KEY(`package_name`, `version`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "packageName",
|
"fieldPath": "packageName",
|
||||||
@ -175,8 +175,8 @@
|
|||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "file",
|
"fieldPath": "directory",
|
||||||
"columnName": "file",
|
"columnName": "directory",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
}
|
}
|
||||||
@ -295,12 +295,119 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "option_groups",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "patchBundle",
|
||||||
|
"columnName": "patch_bundle",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_option_groups_patch_bundle_package_name",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"patch_bundle",
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_option_groups_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "patch_bundles",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"patch_bundle"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "options",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`group`, `patch_name`, `key`), FOREIGN KEY(`group`) REFERENCES `option_groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "group",
|
||||||
|
"columnName": "group",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "patchName",
|
||||||
|
"columnName": "patch_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "key",
|
||||||
|
"columnName": "key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group",
|
||||||
|
"patch_name",
|
||||||
|
"key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "option_groups",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"group"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"views": [],
|
"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, '5515d164bc8f713201506d42a02d337f')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '802fa2fda94b930bf0ebb85d195f1022')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,6 +10,7 @@
|
|||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
@ -33,7 +34,7 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.ReVancedManager"
|
android:theme="@style/Theme.ReVancedManager"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
tools:targetApi="33">
|
tools:targetApi="34">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
@ -49,6 +50,17 @@
|
|||||||
<service android:name=".service.InstallService" />
|
<service android:name=".service.InstallService" />
|
||||||
<service android:name=".service.UninstallService" />
|
<service android:name=".service.UninstallService" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="patching"
|
||||||
|
/>
|
||||||
|
</service>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.startup.InitializationProvider"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
android:authorities="${applicationId}.androidx-startup"
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
@ -3,4 +3,4 @@ name=__LABEL__ ReVanced
|
|||||||
version=__VERSION__
|
version=__VERSION__
|
||||||
versionCode=0
|
versionCode=0
|
||||||
author=ReVanced
|
author=ReVanced
|
||||||
description=Mounts the patched apk on top of the original apk
|
description=Mounts the patched APK on top of the original one
|
@ -5,29 +5,38 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Update
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||||
import app.revanced.manager.ui.destination.Destination
|
import app.revanced.manager.ui.destination.Destination
|
||||||
import app.revanced.manager.ui.screen.AppInfoScreen
|
import app.revanced.manager.ui.destination.SettingsDestination
|
||||||
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
|
||||||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||||
import app.revanced.manager.ui.screen.DashboardScreen
|
import app.revanced.manager.ui.screen.DashboardScreen
|
||||||
|
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
|
||||||
import app.revanced.manager.ui.screen.InstallerScreen
|
import app.revanced.manager.ui.screen.InstallerScreen
|
||||||
import app.revanced.manager.ui.screen.PatchesSelectorScreen
|
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
||||||
import app.revanced.manager.ui.screen.SettingsScreen
|
import app.revanced.manager.ui.screen.SettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
||||||
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
||||||
import app.revanced.manager.ui.theme.Theme
|
import app.revanced.manager.ui.theme.Theme
|
||||||
import app.revanced.manager.ui.viewmodel.MainViewModel
|
import app.revanced.manager.ui.viewmodel.MainViewModel
|
||||||
|
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||||
import dev.olshevski.navigation.reimagined.AnimatedNavHost
|
import dev.olshevski.navigation.reimagined.AnimatedNavHost
|
||||||
import dev.olshevski.navigation.reimagined.NavBackHandler
|
import dev.olshevski.navigation.reimagined.NavBackHandler
|
||||||
import dev.olshevski.navigation.reimagined.navigate
|
import dev.olshevski.navigation.reimagined.navigate
|
||||||
import dev.olshevski.navigation.reimagined.pop
|
import dev.olshevski.navigation.reimagined.pop
|
||||||
import dev.olshevski.navigation.reimagined.popUpTo
|
import dev.olshevski.navigation.reimagined.popUpTo
|
||||||
import dev.olshevski.navigation.reimagined.rememberNavController
|
import dev.olshevski.navigation.reimagined.rememberNavController
|
||||||
import org.koin.androidx.compose.getViewModel
|
|
||||||
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.compose.getViewModel as getComposeViewModel
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@ -36,7 +45,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
|
|
||||||
val vm: MainViewModel = getActivityViewModel()
|
val vm: MainViewModel = getAndroidViewModel()
|
||||||
|
|
||||||
|
vm.importLegacySettings(this)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val theme by vm.prefs.theme.getAsState()
|
val theme by vm.prefs.theme.getAsState()
|
||||||
@ -51,9 +62,32 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
NavBackHandler(navController)
|
NavBackHandler(navController)
|
||||||
|
|
||||||
val showAutoUpdatesDialog by vm.prefs.showAutoUpdatesDialog.getAsState()
|
val firstLaunch by vm.prefs.firstLaunch.getAsState()
|
||||||
if (showAutoUpdatesDialog) {
|
|
||||||
AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
|
if (firstLaunch) AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
|
||||||
|
|
||||||
|
vm.updatedManagerVersion?.let {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = vm::dismissUpdateDialog,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
vm.dismissUpdateDialog()
|
||||||
|
navController.navigate(Destination.Settings(SettingsDestination.Update(false)))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.update))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = vm::dismissUpdateDialog) {
|
||||||
|
Text(stringResource(R.string.dismiss_temporary))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = { Icon(Icons.Outlined.Update, null) },
|
||||||
|
title = { Text(stringResource(R.string.update_available_dialog_title)) },
|
||||||
|
text = { Text(stringResource(R.string.update_available_dialog_description, it)) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedNavHost(
|
AnimatedNavHost(
|
||||||
@ -61,26 +95,44 @@ class MainActivity : ComponentActivity() {
|
|||||||
) { destination ->
|
) { destination ->
|
||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Dashboard -> DashboardScreen(
|
is Destination.Dashboard -> DashboardScreen(
|
||||||
onSettingsClick = { navController.navigate(Destination.Settings) },
|
onSettingsClick = { navController.navigate(Destination.Settings()) },
|
||||||
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
|
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
|
||||||
onAppClick = { installedApp -> navController.navigate(Destination.ApplicationInfo(installedApp)) }
|
onAppClick = { installedApp ->
|
||||||
|
navController.navigate(
|
||||||
|
Destination.InstalledApplicationInfo(
|
||||||
|
installedApp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
is Destination.ApplicationInfo -> AppInfoScreen(
|
is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen(
|
||||||
onPatchClick = { packageName, patchesSelection ->
|
onPatchClick = { packageName, patchesSelection ->
|
||||||
navController.navigate(Destination.VersionSelector(packageName, patchesSelection))
|
navController.navigate(
|
||||||
|
Destination.VersionSelector(
|
||||||
|
packageName,
|
||||||
|
patchesSelection
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onBackClick = { navController.pop() },
|
onBackClick = { navController.pop() },
|
||||||
viewModel = getViewModel { parametersOf(destination.installedApp) }
|
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is Destination.Settings -> SettingsScreen(
|
is Destination.Settings -> SettingsScreen(
|
||||||
onBackClick = { navController.pop() }
|
onBackClick = { navController.pop() },
|
||||||
|
startDestination = destination.startDestination
|
||||||
)
|
)
|
||||||
|
|
||||||
is Destination.AppSelector -> AppSelectorScreen(
|
is Destination.AppSelector -> AppSelectorScreen(
|
||||||
onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
|
onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
|
||||||
onStorageClick = { navController.navigate(Destination.PatchesSelector(it)) },
|
onStorageClick = {
|
||||||
|
navController.navigate(
|
||||||
|
Destination.SelectedApplicationInfo(
|
||||||
|
it
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
onBackClick = { navController.pop() }
|
onBackClick = { navController.pop() }
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -88,36 +140,46 @@ class MainActivity : ComponentActivity() {
|
|||||||
onBackClick = { navController.pop() },
|
onBackClick = { navController.pop() },
|
||||||
onAppClick = { selectedApp ->
|
onAppClick = { selectedApp ->
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
Destination.PatchesSelector(
|
Destination.SelectedApplicationInfo(
|
||||||
selectedApp,
|
selectedApp,
|
||||||
|
destination.patchesSelection,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
viewModel = getComposeViewModel {
|
||||||
|
parametersOf(
|
||||||
|
destination.packageName,
|
||||||
|
destination.patchesSelection
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
|
||||||
|
onPatchClick = { app, patches, options ->
|
||||||
|
navController.navigate(
|
||||||
|
Destination.Installer(
|
||||||
|
app, patches, options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onBackClick = navController::pop,
|
||||||
|
vm = getComposeViewModel {
|
||||||
|
parametersOf(
|
||||||
|
SelectedAppInfoViewModel.Params(
|
||||||
|
destination.selectedApp,
|
||||||
destination.patchesSelection
|
destination.patchesSelection
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
viewModel = getViewModel { parametersOf(destination.packageName, destination.patchesSelection) }
|
|
||||||
)
|
|
||||||
|
|
||||||
is Destination.PatchesSelector -> PatchesSelectorScreen(
|
|
||||||
onBackClick = { navController.pop() },
|
|
||||||
onPatchClick = { patches, options ->
|
|
||||||
navController.navigate(
|
|
||||||
Destination.Installer(
|
|
||||||
destination.selectedApp,
|
|
||||||
patches,
|
|
||||||
options
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
vm = getViewModel { parametersOf(destination) }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is Destination.Installer -> InstallerScreen(
|
is Destination.Installer -> InstallerScreen(
|
||||||
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
|
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
|
||||||
vm = getViewModel { parametersOf(destination) }
|
vm = getComposeViewModel { parametersOf(destination) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,15 +13,19 @@ import app.revanced.manager.data.room.selection.SelectedPatch
|
|||||||
import app.revanced.manager.data.room.selection.SelectionDao
|
import app.revanced.manager.data.room.selection.SelectionDao
|
||||||
import app.revanced.manager.data.room.bundles.PatchBundleDao
|
import app.revanced.manager.data.room.bundles.PatchBundleDao
|
||||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
import app.revanced.manager.data.room.options.Option
|
||||||
|
import app.revanced.manager.data.room.options.OptionDao
|
||||||
|
import app.revanced.manager.data.room.options.OptionGroup
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class], version = 1)
|
@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class], version = 1)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun patchBundleDao(): PatchBundleDao
|
abstract fun patchBundleDao(): PatchBundleDao
|
||||||
abstract fun selectionDao(): SelectionDao
|
abstract fun selectionDao(): SelectionDao
|
||||||
abstract fun downloadedAppDao(): DownloadedAppDao
|
abstract fun downloadedAppDao(): DownloadedAppDao
|
||||||
abstract fun installedAppDao(): InstalledAppDao
|
abstract fun installedAppDao(): InstalledAppDao
|
||||||
|
abstract fun optionDao(): OptionDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun generateUid() = Random.Default.nextInt()
|
fun generateUid() = Random.Default.nextInt()
|
||||||
|
@ -16,5 +16,5 @@ class Converters {
|
|||||||
fun fileFromString(value: String) = File(value)
|
fun fileFromString(value: String) = File(value)
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun fileToString(file: File): String = file.absolutePath
|
fun fileToString(file: File): String = file.path
|
||||||
}
|
}
|
@ -11,5 +11,5 @@ import java.io.File
|
|||||||
data class DownloadedApp(
|
data class DownloadedApp(
|
||||||
@ColumnInfo(name = "package_name") val packageName: String,
|
@ColumnInfo(name = "package_name") val packageName: String,
|
||||||
@ColumnInfo(name = "version") val version: String,
|
@ColumnInfo(name = "version") val version: String,
|
||||||
@ColumnInfo(name = "file") val file: File,
|
@ColumnInfo(name = "directory") val directory: File,
|
||||||
)
|
)
|
@ -6,6 +6,7 @@ import androidx.room.Insert
|
|||||||
import androidx.room.MapInfo
|
import androidx.room.MapInfo
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Upsert
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@ -24,17 +25,21 @@ interface InstalledAppDao {
|
|||||||
suspend fun getPatchesSelection(packageName: String): Map<Int, List<String>>
|
suspend fun getPatchesSelection(packageName: String): Map<Int, List<String>>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
suspend fun insertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) {
|
suspend fun upsertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) {
|
||||||
insertApp(installedApp)
|
upsertApp(installedApp)
|
||||||
|
deleteAppliedPatches(installedApp.currentPackageName)
|
||||||
insertAppliedPatches(appliedPatches)
|
insertAppliedPatches(appliedPatches)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Insert
|
@Upsert
|
||||||
suspend fun insertApp(installedApp: InstalledApp)
|
suspend fun upsertApp(installedApp: InstalledApp)
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insertAppliedPatches(appliedPatches: List<AppliedPatch>)
|
suspend fun insertAppliedPatches(appliedPatches: List<AppliedPatch>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM applied_patch WHERE package_name = :packageName")
|
||||||
|
suspend fun deleteAppliedPatches(packageName: String)
|
||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
suspend fun delete(installedApp: InstalledApp)
|
suspend fun delete(installedApp: InstalledApp)
|
||||||
}
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package app.revanced.manager.data.room.options
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "options",
|
||||||
|
primaryKeys = ["group", "patch_name", "key"],
|
||||||
|
foreignKeys = [ForeignKey(
|
||||||
|
OptionGroup::class,
|
||||||
|
parentColumns = ["uid"],
|
||||||
|
childColumns = ["group"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
data class Option(
|
||||||
|
@ColumnInfo(name = "group") val group: Int,
|
||||||
|
@ColumnInfo(name = "patch_name") val patchName: String,
|
||||||
|
@ColumnInfo(name = "key") val key: String,
|
||||||
|
// Encoded as Json.
|
||||||
|
@ColumnInfo(name = "value") val value: String,
|
||||||
|
)
|
@ -0,0 +1,51 @@
|
|||||||
|
package app.revanced.manager.data.room.options
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.MapInfo
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class OptionDao {
|
||||||
|
@Transaction
|
||||||
|
@MapInfo(keyColumn = "patch_bundle")
|
||||||
|
@Query(
|
||||||
|
"SELECT patch_bundle, `group`, patch_name, `key`, value FROM option_groups" +
|
||||||
|
" LEFT JOIN options ON uid = options.`group`" +
|
||||||
|
" WHERE package_name = :packageName"
|
||||||
|
)
|
||||||
|
abstract suspend fun getOptions(packageName: String): Map<Int, List<Option>>
|
||||||
|
|
||||||
|
@Query("SELECT uid FROM option_groups WHERE patch_bundle = :bundleUid AND package_name = :packageName")
|
||||||
|
abstract suspend fun getGroupId(bundleUid: Int, packageName: String): Int?
|
||||||
|
|
||||||
|
@Query("SELECT package_name FROM option_groups")
|
||||||
|
abstract fun getPackagesWithOptions(): Flow<List<String>>
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
abstract suspend fun createOptionGroup(group: OptionGroup)
|
||||||
|
|
||||||
|
@Query("DELETE FROM option_groups WHERE patch_bundle = :uid")
|
||||||
|
abstract suspend fun clearForPatchBundle(uid: Int)
|
||||||
|
|
||||||
|
@Query("DELETE FROM option_groups WHERE package_name = :packageName")
|
||||||
|
abstract suspend fun clearForPackage(packageName: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM option_groups")
|
||||||
|
abstract suspend fun reset()
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
protected abstract suspend fun insertOptions(patches: List<Option>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM options WHERE `group` = :groupId")
|
||||||
|
protected abstract suspend fun clearGroup(groupId: Int)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open suspend fun updateOptions(options: Map<Int, List<Option>>) =
|
||||||
|
options.forEach { (groupId, options) ->
|
||||||
|
clearGroup(groupId)
|
||||||
|
insertOptions(options)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package app.revanced.manager.data.room.options
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "option_groups",
|
||||||
|
foreignKeys = [ForeignKey(
|
||||||
|
PatchBundleEntity::class,
|
||||||
|
parentColumns = ["uid"],
|
||||||
|
childColumns = ["patch_bundle"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)],
|
||||||
|
indices = [Index(value = ["patch_bundle", "package_name"], unique = true)]
|
||||||
|
)
|
||||||
|
data class OptionGroup(
|
||||||
|
@PrimaryKey val uid: Int,
|
||||||
|
@ColumnInfo(name = "patch_bundle") val patchBundle: Int,
|
||||||
|
@ColumnInfo(name = "package_name") val packageName: String
|
||||||
|
)
|
@ -49,7 +49,7 @@ abstract class SelectionDao {
|
|||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
open suspend fun updateSelections(selections: Map<Int, Set<String>>) =
|
open suspend fun updateSelections(selections: Map<Int, Set<String>>) =
|
||||||
selections.map { (selectionUid, patches) ->
|
selections.forEach { (selectionUid, patches) ->
|
||||||
clearSelection(selectionUid)
|
clearSelection(selectionUid)
|
||||||
selectPatches(patches.map { SelectedPatch(selectionUid, it) })
|
selectPatches(patches.map { SelectedPatch(selectionUid, it) })
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package app.revanced.manager.di
|
package app.revanced.manager.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import app.revanced.manager.BuildConfig
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.engine.okhttp.*
|
import io.ktor.client.engine.okhttp.*
|
||||||
import io.ktor.client.plugins.HttpTimeout
|
import io.ktor.client.plugins.HttpTimeout
|
||||||
|
import io.ktor.client.plugins.UserAgent
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@ -40,6 +42,9 @@ val httpModule = module {
|
|||||||
install(HttpTimeout) {
|
install(HttpTimeout) {
|
||||||
socketTimeoutMillis = 10000
|
socketTimeoutMillis = 10000
|
||||||
}
|
}
|
||||||
|
install(UserAgent) {
|
||||||
|
agent = "ReVanced-Manager/${BuildConfig.VERSION_CODE}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun provideJson() = Json {
|
fun provideJson() = Json {
|
||||||
|
@ -17,7 +17,11 @@ val repositoryModule = module {
|
|||||||
singleOf(::NetworkInfo)
|
singleOf(::NetworkInfo)
|
||||||
singleOf(::PatchBundlePersistenceRepository)
|
singleOf(::PatchBundlePersistenceRepository)
|
||||||
singleOf(::PatchSelectionRepository)
|
singleOf(::PatchSelectionRepository)
|
||||||
singleOf(::PatchBundleRepository)
|
singleOf(::PatchOptionsRepository)
|
||||||
|
singleOf(::PatchBundleRepository) {
|
||||||
|
// It is best to load patch bundles ASAP
|
||||||
|
createdAtStart()
|
||||||
|
}
|
||||||
singleOf(::WorkerRepository)
|
singleOf(::WorkerRepository)
|
||||||
singleOf(::DownloadedAppRepository)
|
singleOf(::DownloadedAppRepository)
|
||||||
singleOf(::InstalledAppRepository)
|
singleOf(::InstalledAppRepository)
|
||||||
|
@ -7,17 +7,18 @@ import org.koin.dsl.module
|
|||||||
val viewModelModule = module {
|
val viewModelModule = module {
|
||||||
viewModelOf(::MainViewModel)
|
viewModelOf(::MainViewModel)
|
||||||
viewModelOf(::DashboardViewModel)
|
viewModelOf(::DashboardViewModel)
|
||||||
|
viewModelOf(::SelectedAppInfoViewModel)
|
||||||
viewModelOf(::PatchesSelectorViewModel)
|
viewModelOf(::PatchesSelectorViewModel)
|
||||||
viewModelOf(::SettingsViewModel)
|
viewModelOf(::SettingsViewModel)
|
||||||
viewModelOf(::AdvancedSettingsViewModel)
|
viewModelOf(::AdvancedSettingsViewModel)
|
||||||
viewModelOf(::AppSelectorViewModel)
|
viewModelOf(::AppSelectorViewModel)
|
||||||
viewModelOf(::VersionSelectorViewModel)
|
viewModelOf(::VersionSelectorViewModel)
|
||||||
viewModelOf(::InstallerViewModel)
|
viewModelOf(::InstallerViewModel)
|
||||||
viewModelOf(::UpdateProgressViewModel)
|
viewModelOf(::UpdateViewModel)
|
||||||
viewModelOf(::ManagerUpdateChangelogViewModel)
|
viewModelOf(::ChangelogsViewModel)
|
||||||
viewModelOf(::ImportExportViewModel)
|
viewModelOf(::ImportExportViewModel)
|
||||||
viewModelOf(::ContributorViewModel)
|
viewModelOf(::ContributorViewModel)
|
||||||
viewModelOf(::DownloadsViewModel)
|
viewModelOf(::DownloadsViewModel)
|
||||||
viewModelOf(::InstalledAppsViewModel)
|
viewModelOf(::InstalledAppsViewModel)
|
||||||
viewModelOf(::AppInfoViewModel)
|
viewModelOf(::InstalledAppInfoViewModel)
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,10 @@ import java.nio.file.StandardCopyOption
|
|||||||
class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSource(name, id, directory) {
|
class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSource(name, id, directory) {
|
||||||
suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) {
|
suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
patches?.let {
|
patches?.let { inputStream ->
|
||||||
Files.copy(it, patchesFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
patchBundleOutputStream().use { outputStream ->
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
integrations?.let {
|
integrations?.let {
|
||||||
Files.copy(it, this@LocalPatchBundle.integrationsFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
Files.copy(it, this@LocalPatchBundle.integrationsFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
package app.revanced.manager.domain.bundles
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import app.revanced.manager.patcher.patch.PatchBundle
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [PatchBundle] source.
|
* A [PatchBundle] source.
|
||||||
@ -23,12 +26,23 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
|
|||||||
*/
|
*/
|
||||||
fun hasInstalled() = patchesFile.exists()
|
fun hasInstalled() = patchesFile.exists()
|
||||||
|
|
||||||
|
protected fun patchBundleOutputStream(): OutputStream = with(patchesFile) {
|
||||||
|
// Android 14+ requires dex containers to be readonly.
|
||||||
|
try {
|
||||||
|
setWritable(true, true)
|
||||||
|
outputStream()
|
||||||
|
} finally {
|
||||||
|
setReadOnly()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun load(): State {
|
private fun load(): State {
|
||||||
if (!hasInstalled()) return State.Missing
|
if (!hasInstalled()) return State.Missing
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
|
State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
|
Log.e(tag, "Failed to load patch bundle $name", t)
|
||||||
State.Failed(t)
|
State.Failed(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,7 +54,7 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
|
|||||||
sealed interface State {
|
sealed interface State {
|
||||||
fun patchBundleOrNull(): PatchBundle? = null
|
fun patchBundleOrNull(): PatchBundle? = null
|
||||||
|
|
||||||
object Missing : State
|
data object Missing : State
|
||||||
data class Failed(val throwable: Throwable) : State
|
data class Failed(val throwable: Throwable) : State
|
||||||
data class Loaded(val bundle: PatchBundle) : State {
|
data class Loaded(val bundle: PatchBundle) : State {
|
||||||
override fun patchBundleOrNull() = bundle
|
override fun patchBundleOrNull() = bundle
|
||||||
|
@ -33,16 +33,19 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
|
|||||||
private suspend fun download(info: BundleInfo) = withContext(Dispatchers.IO) {
|
private suspend fun download(info: BundleInfo) = withContext(Dispatchers.IO) {
|
||||||
val (patches, integrations) = info
|
val (patches, integrations) = info
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
mapOf(
|
launch {
|
||||||
patches.url to patchesFile,
|
patchBundleOutputStream().use {
|
||||||
integrations.url to integrationsFile
|
http.streamTo(it) {
|
||||||
).forEach { (asset, file) ->
|
url(patches.url)
|
||||||
launch {
|
|
||||||
http.download(file) {
|
|
||||||
url(asset)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
http.download(integrationsFile) {
|
||||||
|
url(integrations.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveVersion(patches.version, integrations.version)
|
saveVersion(patches.version, integrations.version)
|
||||||
@ -101,7 +104,7 @@ class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
|||||||
override suspend fun getLatestInfo() = coroutineScope {
|
override suspend fun getLatestInfo() = coroutineScope {
|
||||||
fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) {
|
fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) {
|
||||||
api
|
api
|
||||||
.getRelease(repo)
|
.getLatestRelease(repo)
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.let {
|
.let {
|
||||||
BundleAsset(it.metadata.tag, it.findAssetByType(mime).downloadUrl)
|
BundleAsset(it.metadata.tag, it.findAssetByType(mime).downloadUrl)
|
||||||
|
@ -12,6 +12,7 @@ class PreferencesManager(
|
|||||||
|
|
||||||
val api = stringPreference("api_url", "https://api.revanced.app")
|
val api = stringPreference("api_url", "https://api.revanced.app")
|
||||||
|
|
||||||
|
val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true)
|
||||||
val allowExperimental = booleanPreference("allow_experimental", false)
|
val allowExperimental = booleanPreference("allow_experimental", false)
|
||||||
|
|
||||||
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
||||||
@ -19,7 +20,7 @@ class PreferencesManager(
|
|||||||
|
|
||||||
val preferSplits = booleanPreference("prefer_splits", false)
|
val preferSplits = booleanPreference("prefer_splits", false)
|
||||||
|
|
||||||
val showAutoUpdatesDialog = booleanPreference("show_auto_updates_dialog", true)
|
val firstLaunch = booleanPreference("first_launch", true)
|
||||||
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
||||||
|
|
||||||
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
||||||
|
@ -1,34 +1,61 @@
|
|||||||
package app.revanced.manager.domain.repository
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
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.apps.downloaded.DownloadedApp
|
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
||||||
|
import app.revanced.manager.network.downloader.AppDownloader
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class DownloadedAppRepository(
|
class DownloadedAppRepository(
|
||||||
|
app: Application,
|
||||||
db: AppDatabase
|
db: AppDatabase
|
||||||
) {
|
) {
|
||||||
|
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
|
||||||
private val dao = db.downloadedAppDao()
|
private val dao = db.downloadedAppDao()
|
||||||
|
|
||||||
fun getAll() = dao.getAllApps().distinctUntilChanged()
|
fun getAll() = dao.getAllApps().distinctUntilChanged()
|
||||||
|
|
||||||
suspend fun get(packageName: String, version: String) = dao.get(packageName, version)
|
fun getApkFileForApp(app: DownloadedApp): File = getApkFileForDir(dir.resolve(app.directory))
|
||||||
|
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
|
||||||
|
|
||||||
suspend fun add(
|
suspend fun download(
|
||||||
packageName: String,
|
app: AppDownloader.App,
|
||||||
version: String,
|
preferSplits: Boolean,
|
||||||
file: File
|
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit = {},
|
||||||
) = dao.insert(
|
): File {
|
||||||
DownloadedApp(
|
this.get(app.packageName, app.version)?.let { downloaded ->
|
||||||
packageName = packageName,
|
return getApkFileForApp(downloaded)
|
||||||
version = version,
|
}
|
||||||
file = file
|
|
||||||
)
|
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
||||||
)
|
val relativePath = File(generateUid().toString())
|
||||||
|
val savePath = dir.resolve(relativePath).also { it.mkdirs() }
|
||||||
|
|
||||||
|
try {
|
||||||
|
app.download(savePath, preferSplits, onDownload)
|
||||||
|
|
||||||
|
dao.insert(DownloadedApp(
|
||||||
|
packageName = app.packageName,
|
||||||
|
version = app.version,
|
||||||
|
directory = relativePath,
|
||||||
|
))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
savePath.deleteRecursively()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the Apk file.
|
||||||
|
return getApkFileForDir(savePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun get(packageName: String, version: String) = dao.get(packageName, version)
|
||||||
|
|
||||||
suspend fun delete(downloadedApps: Collection<DownloadedApp>) {
|
suspend fun delete(downloadedApps: Collection<DownloadedApp>) {
|
||||||
downloadedApps.forEach {
|
downloadedApps.forEach {
|
||||||
it.file.deleteRecursively()
|
dir.resolve(it.directory).deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
dao.delete(downloadedApps)
|
dao.delete(downloadedApps)
|
||||||
|
@ -19,14 +19,14 @@ class InstalledAppRepository(
|
|||||||
suspend fun getAppliedPatches(packageName: String): PatchesSelection =
|
suspend fun getAppliedPatches(packageName: String): PatchesSelection =
|
||||||
dao.getPatchesSelection(packageName).mapValues { (_, patches) -> patches.toSet() }
|
dao.getPatchesSelection(packageName).mapValues { (_, patches) -> patches.toSet() }
|
||||||
|
|
||||||
suspend fun add(
|
suspend fun addOrUpdate(
|
||||||
currentPackageName: String,
|
currentPackageName: String,
|
||||||
originalPackageName: String,
|
originalPackageName: String,
|
||||||
version: String,
|
version: String,
|
||||||
installType: InstallType,
|
installType: InstallType,
|
||||||
patchesSelection: PatchesSelection
|
patchesSelection: PatchesSelection
|
||||||
) {
|
) {
|
||||||
dao.insertApp(
|
dao.upsertApp(
|
||||||
InstalledApp(
|
InstalledApp(
|
||||||
currentPackageName = currentPackageName,
|
currentPackageName = currentPackageName,
|
||||||
originalPackageName = originalPackageName,
|
originalPackageName = originalPackageName,
|
||||||
|
@ -3,6 +3,7 @@ package app.revanced.manager.domain.repository
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
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.bundles.PatchBundleEntity
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
import app.revanced.manager.domain.bundles.APIPatchBundle
|
import app.revanced.manager.domain.bundles.APIPatchBundle
|
||||||
@ -13,18 +14,19 @@ import app.revanced.manager.domain.bundles.RemotePatchBundle
|
|||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
import app.revanced.manager.util.flatMapLatestAndCombine
|
import app.revanced.manager.util.flatMapLatestAndCombine
|
||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
|
import app.revanced.manager.util.uiSafe
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
class PatchBundleRepository(
|
class PatchBundleRepository(
|
||||||
app: Application,
|
private val app: Application,
|
||||||
private val persistenceRepo: PatchBundlePersistenceRepository,
|
private val persistenceRepo: PatchBundlePersistenceRepository,
|
||||||
private val networkInfo: NetworkInfo,
|
private val networkInfo: NetworkInfo,
|
||||||
) {
|
) {
|
||||||
@ -124,20 +126,24 @@ class PatchBundleRepository(
|
|||||||
reload()
|
reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun redownloadRemoteBundles() = getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() }
|
suspend fun redownloadRemoteBundles() =
|
||||||
|
getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() }
|
||||||
|
|
||||||
suspend fun updateCheck() = supervisorScope {
|
suspend fun updateCheck() =
|
||||||
if (!networkInfo.isSafe()) {
|
uiSafe(app, R.string.source_download_fail, "Failed to update bundles") {
|
||||||
Log.d(tag, "Skipping update check because the network is down or metered.")
|
coroutineScope {
|
||||||
return@supervisorScope
|
if (!networkInfo.isSafe()) {
|
||||||
}
|
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||||
|
return@coroutineScope
|
||||||
|
}
|
||||||
|
|
||||||
getBundlesByType<RemotePatchBundle>().forEach {
|
getBundlesByType<RemotePatchBundle>().forEach {
|
||||||
launch {
|
launch {
|
||||||
if (!it.propsFlow().first().autoUpdate) return@launch
|
if (!it.propsFlow().first().autoUpdate) return@launch
|
||||||
Log.d(tag, "Updating patch bundle: ${it.name}")
|
Log.d(tag, "Updating patch bundle: ${it.name}")
|
||||||
it.update()
|
it.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.data.room.options.Option
|
||||||
|
import app.revanced.manager.data.room.options.OptionGroup
|
||||||
|
import app.revanced.manager.util.Options
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonNull
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.booleanOrNull
|
||||||
|
import kotlinx.serialization.json.floatOrNull
|
||||||
|
import kotlinx.serialization.json.intOrNull
|
||||||
|
|
||||||
|
class PatchOptionsRepository(db: AppDatabase) {
|
||||||
|
private val dao = db.optionDao()
|
||||||
|
|
||||||
|
private suspend fun getOrCreateGroup(bundleUid: Int, packageName: String) =
|
||||||
|
dao.getGroupId(bundleUid, packageName) ?: OptionGroup(
|
||||||
|
uid = AppDatabase.generateUid(),
|
||||||
|
patchBundle = bundleUid,
|
||||||
|
packageName = packageName
|
||||||
|
).also { dao.createOptionGroup(it) }.uid
|
||||||
|
|
||||||
|
suspend fun getOptions(packageName: String): Options {
|
||||||
|
val options = dao.getOptions(packageName)
|
||||||
|
// Bundle -> Patches
|
||||||
|
return buildMap<Int, MutableMap<String, MutableMap<String, Any?>>>(options.size) {
|
||||||
|
options.forEach { (sourceUid, bundlePatchOptionsList) ->
|
||||||
|
// Patches -> Patch options
|
||||||
|
this[sourceUid] = bundlePatchOptionsList.fold(mutableMapOf()) { bundlePatchOptions, option ->
|
||||||
|
val patchOptions = bundlePatchOptions.getOrPut(option.patchName, ::mutableMapOf)
|
||||||
|
|
||||||
|
patchOptions[option.key] = deserialize(option.value)
|
||||||
|
|
||||||
|
bundlePatchOptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveOptions(packageName: String, options: Options) =
|
||||||
|
dao.updateOptions(options.entries.associate { (sourceUid, bundlePatchOptions) ->
|
||||||
|
val groupId = getOrCreateGroup(sourceUid, packageName)
|
||||||
|
|
||||||
|
groupId to bundlePatchOptions.flatMap { (patchName, patchOptions) ->
|
||||||
|
patchOptions.mapNotNull { (key, value) ->
|
||||||
|
val serialized = serialize(value)
|
||||||
|
?: return@mapNotNull null // Don't save options that we can't serialize.
|
||||||
|
|
||||||
|
Option(groupId, patchName, key, serialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fun getPackagesWithSavedOptions() =
|
||||||
|
dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged()
|
||||||
|
|
||||||
|
suspend fun clearOptionsForPackage(packageName: String) = dao.clearForPackage(packageName)
|
||||||
|
suspend fun clearOptionsForPatchBundle(uid: Int) = dao.clearForPatchBundle(uid)
|
||||||
|
suspend fun reset() = dao.reset()
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
fun deserialize(value: String): Any? {
|
||||||
|
val primitive = Json.decodeFromString<JsonPrimitive>(value)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
primitive.isString -> primitive.content
|
||||||
|
primitive is JsonNull -> null
|
||||||
|
else -> primitive.booleanOrNull ?: primitive.intOrNull ?: primitive.floatOrNull
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun serialize(value: Any?): String? {
|
||||||
|
val primitive = when (value) {
|
||||||
|
null -> JsonNull
|
||||||
|
is String -> JsonPrimitive(value)
|
||||||
|
is Int -> JsonPrimitive(value)
|
||||||
|
is Float -> JsonPrimitive(value)
|
||||||
|
is Boolean -> JsonPrimitive(value)
|
||||||
|
else -> return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json.encodeToString(primitive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,8 @@
|
|||||||
package app.revanced.manager.network.api
|
package app.revanced.manager.network.api
|
||||||
|
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.network.dto.Asset
|
|
||||||
import app.revanced.manager.network.dto.ReVancedLatestRelease
|
|
||||||
import app.revanced.manager.network.dto.ReVancedRelease
|
import app.revanced.manager.network.dto.ReVancedRelease
|
||||||
import app.revanced.manager.network.service.ReVancedService
|
import app.revanced.manager.network.service.ReVancedService
|
||||||
import app.revanced.manager.network.utils.getOrThrow
|
|
||||||
import app.revanced.manager.network.utils.transform
|
import app.revanced.manager.network.utils.transform
|
||||||
|
|
||||||
class ReVancedAPI(
|
class ReVancedAPI(
|
||||||
@ -16,7 +13,9 @@ class ReVancedAPI(
|
|||||||
|
|
||||||
suspend fun getContributors() = service.getContributors(apiUrl()).transform { it.repositories }
|
suspend fun getContributors() = service.getContributors(apiUrl()).transform { it.repositories }
|
||||||
|
|
||||||
suspend fun getRelease(name: String) = service.getRelease(apiUrl(), name).transform { it.release }
|
suspend fun getLatestRelease(name: String) = service.getLatestRelease(apiUrl(), name).transform { it.release }
|
||||||
|
|
||||||
|
suspend fun getReleases(name: String) = service.getReleases(apiUrl(), name).transform { it.releases }
|
||||||
|
|
||||||
companion object Extensions {
|
companion object Extensions {
|
||||||
fun ReVancedRelease.findAssetByType(mime: String) = assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime)
|
fun ReVancedRelease.findAssetByType(mime: String) = assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime)
|
||||||
|
@ -171,7 +171,7 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
saveDirectory: File,
|
saveDirectory: File,
|
||||||
preferSplit: Boolean,
|
preferSplit: Boolean,
|
||||||
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit
|
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit
|
||||||
): File {
|
) {
|
||||||
val variants = httpClient.getHtml { url(apkMirror + downloadLink) }
|
val variants = httpClient.getHtml { url(apkMirror + downloadLink) }
|
||||||
.div {
|
.div {
|
||||||
withClass = "variants-table"
|
withClass = "variants-table"
|
||||||
@ -246,18 +246,10 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val saveLocation = if (variant.apkType == APKType.BUNDLE)
|
val targetFile = saveDirectory.resolve("base.apk")
|
||||||
saveDirectory.resolve(version).also { it.mkdirs() }
|
|
||||||
else
|
|
||||||
saveDirectory.resolve("$version.apk")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val downloadLocation = if (variant.apkType == APKType.BUNDLE)
|
httpClient.download(targetFile) {
|
||||||
saveLocation.resolve("temp.zip")
|
|
||||||
else
|
|
||||||
saveLocation
|
|
||||||
|
|
||||||
httpClient.download(downloadLocation) {
|
|
||||||
url(apkMirror + downloadLink)
|
url(apkMirror + downloadLink)
|
||||||
onDownload { bytesSentTotal, contentLength ->
|
onDownload { bytesSentTotal, contentLength ->
|
||||||
onDownload(bytesSentTotal.div(100000).toFloat().div(10) to contentLength.div(100000).toFloat().div(10))
|
onDownload(bytesSentTotal.div(100000).toFloat().div(10) to contentLength.div(100000).toFloat().div(10))
|
||||||
@ -267,16 +259,11 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
if (variant.apkType == APKType.BUNDLE) {
|
if (variant.apkType == APKType.BUNDLE) {
|
||||||
// TODO: Extract temp.zip
|
// TODO: Extract temp.zip
|
||||||
|
|
||||||
downloadLocation.delete()
|
targetFile.delete()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
saveLocation.deleteRecursively()
|
|
||||||
throw e
|
|
||||||
} finally {
|
} finally {
|
||||||
onDownload(null)
|
onDownload(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveLocation
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ interface AppDownloader {
|
|||||||
saveDirectory: File,
|
saveDirectory: File,
|
||||||
preferSplit: Boolean,
|
preferSplit: Boolean,
|
||||||
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit = {}
|
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit = {}
|
||||||
): File
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -8,6 +8,11 @@ data class ReVancedLatestRelease(
|
|||||||
val release: ReVancedRelease,
|
val release: ReVancedRelease,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ReVancedReleases(
|
||||||
|
val releases: List<ReVancedRelease>
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ReVancedRelease(
|
data class ReVancedRelease(
|
||||||
val metadata: ReVancedReleaseMeta,
|
val metadata: ReVancedReleaseMeta,
|
||||||
@ -28,6 +33,7 @@ data class ReVancedReleaseMeta(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Asset(
|
data class Asset(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
@SerialName("download_count") val downloadCount: Int,
|
||||||
@SerialName("browser_download_url") val downloadUrl: String,
|
@SerialName("browser_download_url") val downloadUrl: String,
|
||||||
@SerialName("content_type") val contentType: String
|
@SerialName("content_type") val contentType: String
|
||||||
)
|
)
|
@ -18,8 +18,11 @@ 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 it.skrape.core.htmlDocument
|
import it.skrape.core.htmlDocument
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Aliucord Authors, DiamondMiner88
|
* @author Aliucord Authors, DiamondMiner88
|
||||||
@ -49,7 +52,10 @@ class HttpService(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.e(tag, "Failed to fetch: API error, http status: ${response.status}, body: $body")
|
Log.e(
|
||||||
|
tag,
|
||||||
|
"Failed to fetch: API error, http status: ${response.status}, body: $body"
|
||||||
|
)
|
||||||
APIResponse.Error(APIError(response.status, body))
|
APIResponse.Error(APIError(response.status, body))
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
@ -59,20 +65,19 @@ class HttpService(
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun download(
|
suspend fun streamTo(
|
||||||
saveLocation: File,
|
outputStream: OutputStream,
|
||||||
builder: HttpRequestBuilder.() -> Unit
|
builder: HttpRequestBuilder.() -> Unit
|
||||||
) {
|
) {
|
||||||
http.prepareGet(builder).execute { httpResponse ->
|
http.prepareGet(builder).execute { httpResponse ->
|
||||||
if (httpResponse.status.isSuccess()) {
|
if (httpResponse.status.isSuccess()) {
|
||||||
|
val channel: ByteReadChannel = httpResponse.body()
|
||||||
saveLocation.outputStream().use { stream ->
|
withContext(Dispatchers.IO) {
|
||||||
val channel: ByteReadChannel = httpResponse.body()
|
|
||||||
while (!channel.isClosedForRead) {
|
while (!channel.isClosedForRead) {
|
||||||
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||||
while (packet.isNotEmpty) {
|
while (packet.isNotEmpty) {
|
||||||
val bytes = packet.readBytes()
|
val bytes = packet.readBytes()
|
||||||
stream.write(bytes)
|
outputStream.write(bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,6 +88,11 @@ class HttpService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun download(
|
||||||
|
saveLocation: File,
|
||||||
|
builder: HttpRequestBuilder.() -> Unit
|
||||||
|
) = saveLocation.outputStream().use { streamTo(it, builder) }
|
||||||
|
|
||||||
suspend fun getHtml(builder: HttpRequestBuilder.() -> Unit) = htmlDocument(
|
suspend fun getHtml(builder: HttpRequestBuilder.() -> Unit) = htmlDocument(
|
||||||
html = http.get(builder).bodyAsText()
|
html = http.get(builder).bodyAsText()
|
||||||
)
|
)
|
||||||
|
@ -2,6 +2,7 @@ package app.revanced.manager.network.service
|
|||||||
|
|
||||||
import app.revanced.manager.network.dto.ReVancedLatestRelease
|
import app.revanced.manager.network.dto.ReVancedLatestRelease
|
||||||
import app.revanced.manager.network.dto.ReVancedGitRepositories
|
import app.revanced.manager.network.dto.ReVancedGitRepositories
|
||||||
|
import app.revanced.manager.network.dto.ReVancedReleases
|
||||||
import app.revanced.manager.network.utils.APIResponse
|
import app.revanced.manager.network.utils.APIResponse
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -10,13 +11,20 @@ import kotlinx.coroutines.withContext
|
|||||||
class ReVancedService(
|
class ReVancedService(
|
||||||
private val client: HttpService,
|
private val client: HttpService,
|
||||||
) {
|
) {
|
||||||
suspend fun getRelease(api: String, repo: String): APIResponse<ReVancedLatestRelease> =
|
suspend fun getLatestRelease(api: String, repo: String): APIResponse<ReVancedLatestRelease> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
client.request {
|
client.request {
|
||||||
url("$api/v2/$repo/releases/latest")
|
url("$api/v2/$repo/releases/latest")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getReleases(api: String, repo: String): APIResponse<ReVancedReleases> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
client.request {
|
||||||
|
url("$api/v2/$repo/releases")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getContributors(api: String): APIResponse<ReVancedGitRepositories> =
|
suspend fun getContributors(api: String): APIResponse<ReVancedGitRepositories> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
client.request {
|
client.request {
|
||||||
|
@ -20,6 +20,7 @@ class Session(
|
|||||||
cacheDir: String,
|
cacheDir: String,
|
||||||
frameworkDir: String,
|
frameworkDir: String,
|
||||||
aaptPath: String,
|
aaptPath: String,
|
||||||
|
multithreadingDexFileWriter: Boolean,
|
||||||
private val logger: ManagerLogger,
|
private val logger: ManagerLogger,
|
||||||
private val input: File,
|
private val input: File,
|
||||||
private val onStepSucceeded: suspend () -> Unit
|
private val onStepSucceeded: suspend () -> Unit
|
||||||
@ -30,7 +31,8 @@ class Session(
|
|||||||
inputFile = input,
|
inputFile = input,
|
||||||
resourceCachePath = tempDir.resolve("aapt-resources"),
|
resourceCachePath = tempDir.resolve("aapt-resources"),
|
||||||
frameworkFileDirectory = frameworkDir,
|
frameworkFileDirectory = frameworkDir,
|
||||||
aaptBinaryPath = aaptPath
|
aaptBinaryPath = aaptPath,
|
||||||
|
multithreadingDexFileWriter = multithreadingDexFileWriter,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
package app.revanced.manager.patcher.aapt
|
package app.revanced.manager.patcher.aapt
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build.SUPPORTED_ABIS as DEVICE_ABIS
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object Aapt {
|
object Aapt {
|
||||||
|
private val WORKING_ABIS = setOf("arm64-v8a", "x86", "x86_64")
|
||||||
|
|
||||||
|
fun supportsDevice() = (DEVICE_ABIS intersect WORKING_ABIS).isNotEmpty()
|
||||||
|
|
||||||
fun binary(context: Context): File? {
|
fun binary(context: Context): File? {
|
||||||
return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
|
return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,10 @@ import java.io.File
|
|||||||
class PatchBundle(private val loader: Iterable<Patch<*>>, val integrations: File?) {
|
class PatchBundle(private val loader: Iterable<Patch<*>>, val integrations: File?) {
|
||||||
constructor(bundleJar: File, integrations: File?) : this(
|
constructor(bundleJar: File, integrations: File?) : this(
|
||||||
object : Iterable<Patch<*>> {
|
object : Iterable<Patch<*>> {
|
||||||
private fun load(): Iterable<Patch<*>> = PatchBundleLoader.Dex(bundleJar, optimizedDexDirectory = null)
|
private fun load(): Iterable<Patch<*>> {
|
||||||
|
bundleJar.setReadOnly()
|
||||||
|
return PatchBundleLoader.Dex(bundleJar, optimizedDexDirectory = null)
|
||||||
|
}
|
||||||
|
|
||||||
override fun iterator(): Iterator<Patch<*>> = load().iterator()
|
override fun iterator(): Iterator<Patch<*>> = load().iterator()
|
||||||
},
|
},
|
||||||
|
@ -56,15 +56,15 @@ data class Option(
|
|||||||
val key: String,
|
val key: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
val required: Boolean,
|
val required: Boolean,
|
||||||
val type: Class<out PatchOption<*>>,
|
val type: String,
|
||||||
val defaultValue: Any?
|
val default: Any?
|
||||||
) {
|
) {
|
||||||
constructor(option: PatchOption<*>) : this(
|
constructor(option: PatchOption<*>) : this(
|
||||||
option.title ?: option.key,
|
option.title ?: option.key,
|
||||||
option.key,
|
option.key,
|
||||||
option.description.orEmpty(),
|
option.description.orEmpty(),
|
||||||
option.required,
|
option.required,
|
||||||
option::class.java,
|
option.valueType,
|
||||||
option.value
|
option.default,
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -26,7 +26,12 @@ class Step(
|
|||||||
val state: State = State.WAITING
|
val state: State = State.WAITING
|
||||||
)
|
)
|
||||||
|
|
||||||
class PatcherProgressManager(context: Context, selectedPatches: List<String>, selectedApp: SelectedApp, downloadProgress: StateFlow<Pair<Float, Float>?>) {
|
class PatcherProgressManager(
|
||||||
|
context: Context,
|
||||||
|
selectedPatches: List<String>,
|
||||||
|
selectedApp: SelectedApp,
|
||||||
|
downloadProgress: StateFlow<Pair<Float, Float>?>
|
||||||
|
) {
|
||||||
val steps = generateSteps(context, selectedPatches, selectedApp, downloadProgress)
|
val steps = generateSteps(context, selectedPatches, selectedApp, downloadProgress)
|
||||||
private var currentStep: StepKey? = StepKey(0, 0)
|
private var currentStep: StepKey? = StepKey(0, 0)
|
||||||
|
|
||||||
@ -87,12 +92,20 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>, se
|
|||||||
selectedPatches.map { SubStep(it) }.toImmutableList()
|
selectedPatches.map { SubStep(it) }.toImmutableList()
|
||||||
)
|
)
|
||||||
|
|
||||||
fun generateSteps(context: Context, selectedPatches: List<String>, selectedApp: SelectedApp, downloadProgress: StateFlow<Pair<Float, Float>?>? = null) = mutableListOf(
|
fun generateSteps(
|
||||||
|
context: Context,
|
||||||
|
selectedPatches: List<String>,
|
||||||
|
selectedApp: SelectedApp,
|
||||||
|
downloadProgress: StateFlow<Pair<Float, Float>?>? = null
|
||||||
|
) = mutableListOf(
|
||||||
Step(
|
Step(
|
||||||
R.string.patcher_step_group_prepare,
|
R.string.patcher_step_group_prepare,
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
SubStep(context.getString(R.string.patcher_step_load_patches)),
|
SubStep(context.getString(R.string.patcher_step_load_patches)),
|
||||||
SubStep("Download apk", progress = downloadProgress).takeIf { selectedApp is SelectedApp.Download },
|
SubStep(
|
||||||
|
"Download apk",
|
||||||
|
progress = downloadProgress
|
||||||
|
).takeIf { selectedApp is SelectedApp.Download },
|
||||||
SubStep(context.getString(R.string.patcher_step_unpack)),
|
SubStep(context.getString(R.string.patcher_step_unpack)),
|
||||||
SubStep(context.getString(R.string.patcher_step_integrations))
|
SubStep(context.getString(R.string.patcher_step_integrations))
|
||||||
).toImmutableList()
|
).toImmutableList()
|
||||||
@ -100,7 +113,10 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>, se
|
|||||||
generatePatchesStep(selectedPatches),
|
generatePatchesStep(selectedPatches),
|
||||||
Step(
|
Step(
|
||||||
R.string.patcher_step_group_saving,
|
R.string.patcher_step_group_saving,
|
||||||
persistentListOf(SubStep(context.getString(R.string.patcher_step_write_patched)))
|
persistentListOf(
|
||||||
|
SubStep(context.getString(R.string.patcher_step_write_patched)),
|
||||||
|
SubStep(context.getString(R.string.patcher_step_sign_apk))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,9 @@ import android.app.NotificationManager
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
@ -17,6 +19,7 @@ import app.revanced.manager.R
|
|||||||
import app.revanced.manager.data.platform.Filesystem
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
import app.revanced.manager.domain.installer.RootInstaller
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
|
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.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
@ -48,6 +51,7 @@ class PatcherWorker(
|
|||||||
private val patchBundleRepository: PatchBundleRepository by inject()
|
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||||
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 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()
|
||||||
@ -71,7 +75,12 @@ class PatcherWorker(
|
|||||||
private fun String.logFmt() = "$logPrefix $this"
|
private fun String.logFmt() = "$logPrefix $this"
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getForegroundInfo() = ForegroundInfo(1, createNotification())
|
override suspend fun getForegroundInfo() =
|
||||||
|
ForegroundInfo(
|
||||||
|
1,
|
||||||
|
createNotification(),
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE else 0
|
||||||
|
)
|
||||||
|
|
||||||
private fun createNotification(): Notification {
|
private fun createNotification(): Notification {
|
||||||
val notificationIntent = Intent(applicationContext, PatcherWorker::class.java)
|
val notificationIntent = Intent(applicationContext, PatcherWorker::class.java)
|
||||||
@ -152,6 +161,8 @@ class PatcherWorker(
|
|||||||
progressFlow.value = progressManager.getProgress().toImmutableList()
|
progressFlow.value = progressManager.getProgress().toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
|
||||||
if (args.input is SelectedApp.Installed) {
|
if (args.input is SelectedApp.Installed) {
|
||||||
@ -168,11 +179,11 @@ class PatcherWorker(
|
|||||||
.mapValues { (_, bundle) -> bundle.patchClasses(args.packageName) }
|
.mapValues { (_, bundle) -> bundle.patchClasses(args.packageName) }
|
||||||
|
|
||||||
// Set all patch options.
|
// Set all patch options.
|
||||||
args.options.forEach { (bundle, configuredPatchOptions) ->
|
args.options.forEach { (bundle, bundlePatchOptions) ->
|
||||||
val patches = allPatches[bundle] ?: return@forEach
|
val patches = allPatches[bundle] ?: return@forEach
|
||||||
configuredPatchOptions.forEach { (patchName, options) ->
|
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||||
val patchOptions = patches.single { it.name == patchName }.options
|
val patchOptions = patches.single { it.name == patchName }.options
|
||||||
options.forEach { (key, value) ->
|
configuredPatchOptions.forEach { (key, value) ->
|
||||||
patchOptions[key] = value
|
patchOptions[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,19 +201,11 @@ class PatcherWorker(
|
|||||||
|
|
||||||
val inputFile = when (val selectedApp = args.input) {
|
val inputFile = when (val selectedApp = args.input) {
|
||||||
is SelectedApp.Download -> {
|
is SelectedApp.Download -> {
|
||||||
val savePath = applicationContext.filesDir.resolve("downloaded-apps")
|
downloadedAppRepository.download(
|
||||||
.resolve(args.input.packageName).also { it.mkdirs() }
|
selectedApp.app,
|
||||||
|
|
||||||
selectedApp.app.download(
|
|
||||||
savePath,
|
|
||||||
prefs.preferSplits.get(),
|
prefs.preferSplits.get(),
|
||||||
onDownload = { downloadProgress.emit(it) }
|
onDownload = { downloadProgress.emit(it) }
|
||||||
).also {
|
).also {
|
||||||
downloadedAppRepository.add(
|
|
||||||
args.input.packageName,
|
|
||||||
args.input.version,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
args.setInputFile(it)
|
args.setInputFile(it)
|
||||||
updateProgress() // Downloading
|
updateProgress() // Downloading
|
||||||
}
|
}
|
||||||
@ -216,13 +219,17 @@ class PatcherWorker(
|
|||||||
fs.tempDir.absolutePath,
|
fs.tempDir.absolutePath,
|
||||||
frameworkPath,
|
frameworkPath,
|
||||||
aaptPath,
|
aaptPath,
|
||||||
|
prefs.multithreadingDexFileWriter.get(),
|
||||||
args.logger,
|
args.logger,
|
||||||
inputFile,
|
inputFile,
|
||||||
onStepSucceeded = ::updateProgress
|
onStepSucceeded = ::updateProgress
|
||||||
).use { session ->
|
).use { session ->
|
||||||
session.run(File(args.output), patches, integrations)
|
session.run(patchedApk, patches, integrations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keystoreManager.sign(patchedApk, File(args.output))
|
||||||
|
updateProgress() // Signing
|
||||||
|
|
||||||
Log.i(tag, "Patching succeeded".logFmt())
|
Log.i(tag, "Patching succeeded".logFmt())
|
||||||
progressManager.success()
|
progressManager.success()
|
||||||
Result.success()
|
Result.success()
|
||||||
@ -232,6 +239,7 @@ class PatcherWorker(
|
|||||||
Result.failure()
|
Result.failure()
|
||||||
} finally {
|
} finally {
|
||||||
updateProgress(false)
|
updateProgress(false)
|
||||||
|
patchedApk.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -29,6 +29,7 @@ class InstallService : Service() {
|
|||||||
else -> {
|
else -> {
|
||||||
sendBroadcast(Intent().apply {
|
sendBroadcast(Intent().apply {
|
||||||
action = APP_INSTALL_ACTION
|
action = APP_INSTALL_ACTION
|
||||||
|
`package` = packageName
|
||||||
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
|
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
|
||||||
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
|
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
|
||||||
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
|
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
|
||||||
|
@ -31,7 +31,7 @@ class UninstallService : Service() {
|
|||||||
else -> {
|
else -> {
|
||||||
sendBroadcast(Intent().apply {
|
sendBroadcast(Intent().apply {
|
||||||
action = APP_UNINSTALL_ACTION
|
action = APP_UNINSTALL_ACTION
|
||||||
|
`package` = packageName
|
||||||
putExtra(EXTRA_UNINSTALL_STATUS, extraStatus)
|
putExtra(EXTRA_UNINSTALL_STATUS, extraStatus)
|
||||||
putExtra(EXTRA_UNINSTALL_STATUS_MESSAGE, extraStatusMessage)
|
putExtra(EXTRA_UNINSTALL_STATUS_MESSAGE, extraStatusMessage)
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppInfo(appInfo: PackageInfo?, placeholderLabel: String? = null, extraContent: @Composable () -> Unit = {}) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
AppIcon(
|
||||||
|
appInfo,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(100.dp)
|
||||||
|
.padding(bottom = 5.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
AppLabel(
|
||||||
|
appInfo,
|
||||||
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
defaultText = placeholderLabel
|
||||||
|
)
|
||||||
|
|
||||||
|
extraContent()
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,6 @@ fun GroupHeader(
|
|||||||
text = title,
|
text = title,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
modifier = Modifier.padding(16.dp).semantics { heading() }.then(modifier)
|
modifier = Modifier.padding(24.dp).semantics { heading() }.then(modifier)
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,113 +1,32 @@
|
|||||||
package app.revanced.manager.ui.component
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.graphics.Color
|
import com.mikepenz.markdown.compose.Markdown
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import com.mikepenz.markdown.model.markdownColor
|
||||||
import app.revanced.manager.util.hexCode
|
import com.mikepenz.markdown.model.markdownTypography
|
||||||
import app.revanced.manager.util.openUrl
|
|
||||||
import com.google.accompanist.web.AccompanistWebViewClient
|
|
||||||
import com.google.accompanist.web.WebView
|
|
||||||
import com.google.accompanist.web.rememberWebViewStateWithHTMLData
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
fun Markdown(
|
fun Markdown(
|
||||||
text: String,
|
text: String
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
) {
|
||||||
val ctx = LocalContext.current
|
val markdown = text.trimIndent()
|
||||||
val state = rememberWebViewStateWithHTMLData(data = generateMdHtml(source = text))
|
|
||||||
val client = remember {
|
|
||||||
object : AccompanistWebViewClient() {
|
|
||||||
override fun shouldOverrideUrlLoading(
|
|
||||||
view: WebView?,
|
|
||||||
request: WebResourceRequest?
|
|
||||||
): Boolean {
|
|
||||||
if (request != null) ctx.openUrl(request.url.toString())
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WebView(
|
Markdown(
|
||||||
state,
|
content = markdown,
|
||||||
modifier = Modifier
|
colors = markdownColor(
|
||||||
.background(Color.Transparent)
|
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
.then(modifier),
|
codeBackground = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
client = client,
|
codeText = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
captureBackPresses = false,
|
),
|
||||||
onCreated = {
|
typography = markdownTypography(
|
||||||
it.setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
h1 = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),
|
||||||
it.isVerticalScrollBarEnabled = false
|
h2 = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
|
||||||
it.isHorizontalScrollBarEnabled = false
|
h3 = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||||
it.setOnTouchListener { _, event -> event.action == MotionEvent.ACTION_MOVE }
|
text = MaterialTheme.typography.bodyMedium,
|
||||||
it.layoutParams = ViewGroup.LayoutParams(
|
list = MaterialTheme.typography.bodyMedium
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
)
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun generateMdHtml(
|
|
||||||
source: String,
|
|
||||||
wrap: Boolean = false,
|
|
||||||
headingColor: Color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
linkColor: Color = MaterialTheme.colorScheme.primary
|
|
||||||
) = remember(source, wrap, headingColor, textColor, linkColor) {
|
|
||||||
"""<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Markdown</title>
|
|
||||||
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;"/>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
color: #${textColor.hexCode};
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #${linkColor.hexCode}!important;
|
|
||||||
}
|
|
||||||
a.anchor {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.highlight pre, pre {
|
|
||||||
word-wrap: ${if (wrap) "break-word" else "normal"};
|
|
||||||
white-space: ${if (wrap) "pre-wrap" else "pre"};
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
color: #${headingColor.hexCode};
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 24px;
|
|
||||||
letter-spacing: 0.15px;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
margin-left: 0px;
|
|
||||||
padding-left: 18px;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
::marker {
|
|
||||||
font-size: 16px;
|
|
||||||
margin-right: 8px;
|
|
||||||
color: #${textColor.hexCode};
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
$source
|
|
||||||
</body>
|
|
||||||
</html>"""
|
|
||||||
}
|
}
|
@ -1,56 +1,177 @@
|
|||||||
package app.revanced.manager.ui.component
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
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.layout.width
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Close
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.revanced.manager.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NotificationCard(
|
fun NotificationCard(
|
||||||
color: Color,
|
isWarning: Boolean = false,
|
||||||
icon: ImageVector,
|
title: String? = null,
|
||||||
text: String,
|
text: String,
|
||||||
content: @Composable () -> Unit
|
icon: ImageVector,
|
||||||
|
actions: (@Composable () -> Unit)?
|
||||||
) {
|
) {
|
||||||
Card(
|
val color =
|
||||||
modifier = Modifier
|
if (isWarning) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(28.dp))
|
NotificationCardInstance(isWarning = isWarning) {
|
||||||
.background(color)
|
Column(
|
||||||
) {
|
modifier = Modifier.padding(if (title != null) 20.dp else 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
if (title != null) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = color,
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = color,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = color,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actions?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NotificationCard(
|
||||||
|
isWarning: Boolean = false,
|
||||||
|
title: String? = null,
|
||||||
|
text: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
onDismiss: (() -> Unit)? = null,
|
||||||
|
primaryAction: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val color =
|
||||||
|
if (isWarning) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
|
||||||
|
NotificationCardInstance(isWarning = isWarning, onClick = primaryAction) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
16.dp,
|
|
||||||
Alignment.Start
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
modifier = Modifier.size(if (title != null) 36.dp else 24.dp),
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
tint = color,
|
||||||
)
|
)
|
||||||
Text(
|
if (title != null) {
|
||||||
modifier = Modifier.width(220.dp),
|
Column(
|
||||||
text = text,
|
modifier = Modifier.weight(1f),
|
||||||
style = MaterialTheme.typography.bodyMedium
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
)
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = color,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (onDismiss != null) {
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Close,
|
||||||
|
contentDescription = stringResource(R.string.close),
|
||||||
|
tint = color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun NotificationCardInstance(
|
||||||
|
isWarning: Boolean = false,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val colors =
|
||||||
|
CardDefaults.cardColors(containerColor = if (isWarning) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primaryContainer)
|
||||||
|
val modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
|
||||||
|
if (onClick != null) {
|
||||||
|
Card(
|
||||||
|
onClick = onClick,
|
||||||
|
colors = colors,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Card(
|
||||||
|
colors = colors,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,12 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
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.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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
|
||||||
import app.revanced.manager.ui.component.TextInputDialog
|
import app.revanced.manager.ui.component.TextInputDialog
|
||||||
|
import app.revanced.manager.util.isDebuggable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BaseBundleDialog(
|
fun BaseBundleDialog(
|
||||||
@ -159,20 +161,18 @@ fun BaseBundleDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val patchesClickable = LocalContext.current.isDebuggable && patchCount > 0
|
||||||
BundleListItem(
|
BundleListItem(
|
||||||
headlineText = stringResource(R.string.patches),
|
headlineText = stringResource(R.string.patches),
|
||||||
supportingText = if (patchCount == 0) stringResource(R.string.no_patches)
|
supportingText = if (patchCount == 0) stringResource(R.string.no_patches)
|
||||||
else stringResource(R.string.patches_available, patchCount),
|
else stringResource(R.string.patches_available, patchCount),
|
||||||
modifier = Modifier.clickable(enabled = patchCount > 0) {
|
modifier = Modifier.clickable(enabled = patchesClickable, onClick = onPatchesClick)
|
||||||
onPatchesClick()
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
if (patchCount > 0) {
|
if (patchesClickable)
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.ArrowRight,
|
Icons.Outlined.ArrowRight,
|
||||||
stringResource(R.string.patches)
|
stringResource(R.string.patches)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
version?.let {
|
version?.let {
|
||||||
|
@ -70,17 +70,10 @@ fun BundlePatchesDialog(
|
|||||||
item {
|
item {
|
||||||
AnimatedVisibility(visible = informationCardVisible) {
|
AnimatedVisibility(visible = informationCardVisible) {
|
||||||
NotificationCard(
|
NotificationCard(
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
icon = Icons.Outlined.Lightbulb,
|
icon = Icons.Outlined.Lightbulb,
|
||||||
text = stringResource(R.string.tap_on_patches)
|
text = stringResource(R.string.tap_on_patches),
|
||||||
) {
|
onDismiss = { informationCardVisible = false }
|
||||||
IconButton(onClick = { informationCardVisible = false }) {
|
)
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Close,
|
|
||||||
contentDescription = stringResource(R.string.close),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,6 @@ import app.revanced.manager.R
|
|||||||
import app.revanced.manager.data.platform.Filesystem
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.patcher.patch.Option
|
import app.revanced.manager.patcher.patch.Option
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.patcher.patch.options.types.*
|
|
||||||
import org.koin.compose.rememberKoinInject
|
import org.koin.compose.rememberKoinInject
|
||||||
|
|
||||||
// Composable functions do not support function references, so we have to use composable lambdas instead.
|
// Composable functions do not support function references, so we have to use composable lambdas instead.
|
||||||
@ -136,69 +135,69 @@ private fun StringOptionDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val StringOption: OptionImpl = { option, value, setValue ->
|
private val unknownOption: OptionImpl = { option, _, _ ->
|
||||||
var showInputDialog by rememberSaveable { mutableStateOf(false) }
|
|
||||||
fun showInputDialog() {
|
|
||||||
showInputDialog = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dismissInputDialog() {
|
|
||||||
showInputDialog = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showInputDialog) {
|
|
||||||
StringOptionDialog(
|
|
||||||
name = option.title,
|
|
||||||
value = value as? String,
|
|
||||||
onSubmit = {
|
|
||||||
dismissInputDialog()
|
|
||||||
setValue(it)
|
|
||||||
},
|
|
||||||
onDismissRequest = ::dismissInputDialog
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
OptionListItem(
|
|
||||||
option = option,
|
|
||||||
onClick = ::showInputDialog
|
|
||||||
) {
|
|
||||||
IconButton(onClick = ::showInputDialog) {
|
|
||||||
Icon(
|
|
||||||
Icons.Outlined.Edit,
|
|
||||||
contentDescription = stringResource(R.string.string_option_icon_description)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val BooleanOption: OptionImpl = { option, value, setValue ->
|
|
||||||
val current = (value as? Boolean) ?: false
|
|
||||||
|
|
||||||
OptionListItem(
|
|
||||||
option = option,
|
|
||||||
onClick = { setValue(!current) }
|
|
||||||
) {
|
|
||||||
Switch(checked = current, onCheckedChange = setValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val UnknownOption: OptionImpl = { option, _, _ ->
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
OptionListItem(
|
OptionListItem(
|
||||||
option = option,
|
option = option,
|
||||||
onClick = { context.toast("Unknown type: ${option.type.name}") },
|
onClick = { context.toast("Unknown type: ${option.type}") },
|
||||||
trailingContent = {})
|
trailingContent = {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val optionImplementations = mapOf<String, OptionImpl>(
|
||||||
|
// These are the only two types that are currently used by the official patches
|
||||||
|
"Boolean" to { option, value, setValue ->
|
||||||
|
val current = (value as? Boolean) ?: false
|
||||||
|
|
||||||
|
OptionListItem(
|
||||||
|
option = option,
|
||||||
|
onClick = { setValue(!current) }
|
||||||
|
) {
|
||||||
|
Switch(checked = current, onCheckedChange = setValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"String" to { option, value, setValue ->
|
||||||
|
var showInputDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
fun showInputDialog() {
|
||||||
|
showInputDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissInputDialog() {
|
||||||
|
showInputDialog = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showInputDialog) {
|
||||||
|
StringOptionDialog(
|
||||||
|
name = option.title,
|
||||||
|
value = value as? String,
|
||||||
|
onSubmit = {
|
||||||
|
dismissInputDialog()
|
||||||
|
setValue(it)
|
||||||
|
},
|
||||||
|
onDismissRequest = ::dismissInputDialog
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionListItem(
|
||||||
|
option = option,
|
||||||
|
onClick = ::showInputDialog
|
||||||
|
) {
|
||||||
|
IconButton(onClick = ::showInputDialog) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.Edit,
|
||||||
|
contentDescription = stringResource(R.string.string_option_icon_description)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OptionItem(option: Option, value: Any?, setValue: (Any?) -> Unit) {
|
fun OptionItem(option: Option, value: Any?, setValue: (Any?) -> Unit) {
|
||||||
val implementation = remember(option.type) {
|
val implementation = remember(option.type) {
|
||||||
when (option.type) {
|
optionImplementations.getOrDefault(
|
||||||
// These are the only two types that are currently used by the official patches.
|
option.type,
|
||||||
StringPatchOption::class.java -> StringOption
|
unknownOption
|
||||||
BooleanPatchOption::class.java -> BooleanOption
|
)
|
||||||
else -> UnknownOption
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation(option, value, setValue)
|
implementation(option, value, setValue)
|
||||||
|
@ -2,9 +2,7 @@ package app.revanced.manager.ui.component.settings
|
|||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@ -37,10 +35,10 @@ fun BooleanItem(
|
|||||||
onValueChange: (Boolean) -> Unit,
|
onValueChange: (Boolean) -> Unit,
|
||||||
@StringRes headline: Int,
|
@StringRes headline: Int,
|
||||||
@StringRes description: Int
|
@StringRes description: Int
|
||||||
) = ListItem(
|
) = SettingsListItem(
|
||||||
modifier = Modifier.clickable { onValueChange(!value) },
|
modifier = Modifier.clickable { onValueChange(!value) },
|
||||||
headlineContent = { Text(stringResource(headline)) },
|
headlineContent = stringResource(headline),
|
||||||
supportingContent = { Text(stringResource(description)) },
|
supportingContent = stringResource(description),
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
Switch(
|
||||||
checked = value,
|
checked = value,
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
package app.revanced.manager.ui.component.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.CalendarToday
|
||||||
|
import androidx.compose.material.icons.outlined.Campaign
|
||||||
|
import androidx.compose.material.icons.outlined.FileDownload
|
||||||
|
import androidx.compose.material.icons.outlined.Sell
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.revanced.manager.ui.component.Markdown
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Changelog(
|
||||||
|
markdown: String,
|
||||||
|
version: String,
|
||||||
|
downloadCount: String,
|
||||||
|
publishDate: String
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 0.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Campaign,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
version.removePrefix("v"),
|
||||||
|
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Tag(
|
||||||
|
Icons.Outlined.Sell,
|
||||||
|
version
|
||||||
|
)
|
||||||
|
Tag(
|
||||||
|
Icons.Outlined.FileDownload,
|
||||||
|
downloadCount
|
||||||
|
)
|
||||||
|
Tag(
|
||||||
|
Icons.Outlined.CalendarToday,
|
||||||
|
publishDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Markdown(
|
||||||
|
markdown,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Tag(icon: ImageVector, text: String) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package app.revanced.manager.ui.component.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ListItemColors
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsListItem(
|
||||||
|
headlineContent: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
overlineContent: @Composable (() -> Unit)? = null,
|
||||||
|
supportingContent: String? = null,
|
||||||
|
leadingContent: @Composable (() -> Unit)? = null,
|
||||||
|
trailingContent: @Composable (() -> Unit)? = null,
|
||||||
|
colors: ListItemColors = ListItemDefaults.colors(),
|
||||||
|
tonalElevation: Dp = ListItemDefaults.Elevation,
|
||||||
|
shadowElevation: Dp = ListItemDefaults.Elevation,
|
||||||
|
) = ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = headlineContent,
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier.then(Modifier.padding(horizontal = 8.dp)),
|
||||||
|
overlineContent = overlineContent,
|
||||||
|
supportingContent = {
|
||||||
|
if (supportingContent != null)
|
||||||
|
Text(
|
||||||
|
text = supportingContent,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = leadingContent,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
colors = colors,
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
shadowElevation = shadowElevation
|
||||||
|
)
|
@ -11,22 +11,22 @@ import kotlinx.parcelize.RawValue
|
|||||||
sealed interface Destination : Parcelable {
|
sealed interface Destination : Parcelable {
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Dashboard : Destination
|
data object Dashboard : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ApplicationInfo(val installedApp: InstalledApp) : Destination
|
data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object AppSelector : Destination
|
data object AppSelector : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Settings : Destination
|
data class Settings(val startDestination: SettingsDestination = SettingsDestination.Settings) : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination
|
data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class PatchesSelector(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
|
data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination
|
data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.manager.ui.destination
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
|
import app.revanced.manager.util.Options
|
||||||
|
import app.revanced.manager.util.PatchesSelection
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.parcelize.RawValue
|
||||||
|
|
||||||
|
sealed interface SelectedAppInfoDestination : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data object Main : SelectedAppInfoDestination
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class PatchesSelector(val app: SelectedApp, val currentSelection: PatchesSelection?, val options: @RawValue Options) : SelectedAppInfoDestination
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object VersionSelector: SelectedAppInfoDestination
|
||||||
|
}
|
@ -27,15 +27,14 @@ sealed interface SettingsDestination : Parcelable {
|
|||||||
object About : SettingsDestination
|
object About : SettingsDestination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object UpdateProgress : SettingsDestination
|
data class Update(val downloadOnScreenEntry: Boolean) : SettingsDestination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object UpdateChangelog : SettingsDestination
|
object Changelogs : SettingsDestination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Contributors: SettingsDestination
|
object Contributors: SettingsDestination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Licenses: SettingsDestination
|
object Licenses: SettingsDestination
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
package app.revanced.manager.ui.model
|
||||||
|
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
|
import app.revanced.manager.util.PatchesSelection
|
||||||
|
import app.revanced.manager.util.flatMapLatestAndCombine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class that contains patch bundle metadata for use by UI code.
|
||||||
|
*/
|
||||||
|
data class BundleInfo(
|
||||||
|
val name: String,
|
||||||
|
val uid: Int,
|
||||||
|
val supported: List<PatchInfo>,
|
||||||
|
val unsupported: List<PatchInfo>,
|
||||||
|
val universal: List<PatchInfo>
|
||||||
|
) {
|
||||||
|
val all = sequence {
|
||||||
|
yieldAll(supported)
|
||||||
|
yieldAll(unsupported)
|
||||||
|
yieldAll(universal)
|
||||||
|
}
|
||||||
|
|
||||||
|
val patchCount get() = supported.size + unsupported.size + universal.size
|
||||||
|
|
||||||
|
fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) {
|
||||||
|
all
|
||||||
|
} else {
|
||||||
|
sequence {
|
||||||
|
yieldAll(supported)
|
||||||
|
yieldAll(universal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object Extensions {
|
||||||
|
inline fun Iterable<BundleInfo>.toPatchSelection(allowUnsupported: Boolean, condition: (Int, PatchInfo) -> Boolean): PatchesSelection = this.associate { bundle ->
|
||||||
|
val patches =
|
||||||
|
bundle.patchSequence(allowUnsupported)
|
||||||
|
.mapNotNullTo(mutableSetOf()) { patch ->
|
||||||
|
patch.name.takeIf {
|
||||||
|
condition(
|
||||||
|
bundle.uid,
|
||||||
|
patch
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle.uid to patches
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String) =
|
||||||
|
sources.flatMapLatestAndCombine(
|
||||||
|
combiner = { it.filterNotNull() }
|
||||||
|
) { source ->
|
||||||
|
// Regenerate bundle information whenever this source updates.
|
||||||
|
source.state.map { state ->
|
||||||
|
val bundle = state.patchBundleOrNull() ?: return@map null
|
||||||
|
|
||||||
|
val supported = mutableListOf<PatchInfo>()
|
||||||
|
val unsupported = mutableListOf<PatchInfo>()
|
||||||
|
val universal = mutableListOf<PatchInfo>()
|
||||||
|
|
||||||
|
bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
|
||||||
|
val targetList = when {
|
||||||
|
it.compatiblePackages == null -> universal
|
||||||
|
it.supportsVersion(
|
||||||
|
packageName,
|
||||||
|
version
|
||||||
|
) -> supported
|
||||||
|
|
||||||
|
else -> unsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
targetList.add(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
BundleInfo(source.name, source.uid, supported, unsupported, universal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@ -65,7 +66,12 @@ fun DashboardScreen(
|
|||||||
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
||||||
val androidContext = LocalContext.current
|
val androidContext = LocalContext.current
|
||||||
|
|
||||||
val pagerState = rememberPagerState()
|
val pagerState = rememberPagerState(
|
||||||
|
initialPage = DashboardPage.DASHBOARD.ordinal,
|
||||||
|
initialPageOffsetFraction = 0f
|
||||||
|
) {
|
||||||
|
DashboardPage.values().size
|
||||||
|
}
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(pagerState.currentPage) {
|
LaunchedEffect(pagerState.currentPage) {
|
||||||
@ -186,7 +192,6 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
pageCount = pages.size,
|
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
userScrollEnabled = true,
|
userScrollEnabled = true,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@ -199,6 +204,13 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
DashboardPage.BUNDLES -> {
|
DashboardPage.BUNDLES -> {
|
||||||
|
BackHandler {
|
||||||
|
if (bundlesSelectable) vm.cancelSourceSelection() else composableScope.launch {
|
||||||
|
pagerState.animateScrollToPage(
|
||||||
|
DashboardPage.DASHBOARD.ordinal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
|
||||||
|
@ -5,9 +5,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@ -21,7 +19,6 @@ import androidx.compose.material.icons.outlined.Update
|
|||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -32,7 +29,6 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
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.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
@ -40,19 +36,19 @@ 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
|
||||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
import app.revanced.manager.ui.component.AppIcon
|
import app.revanced.manager.ui.component.AppInfo
|
||||||
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.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.component.SegmentedButton
|
import app.revanced.manager.ui.component.SegmentedButton
|
||||||
import app.revanced.manager.ui.viewmodel.AppInfoViewModel
|
import app.revanced.manager.ui.viewmodel.InstalledAppInfoViewModel
|
||||||
import app.revanced.manager.util.PatchesSelection
|
import app.revanced.manager.util.PatchesSelection
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppInfoScreen(
|
fun InstalledAppInfoScreen(
|
||||||
onPatchClick: (packageName: String, patchesSelection: PatchesSelection) -> Unit,
|
onPatchClick: (packageName: String, patchesSelection: PatchesSelection) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
viewModel: AppInfoViewModel
|
viewModel: InstalledAppInfoViewModel
|
||||||
) {
|
) {
|
||||||
SideEffect {
|
SideEffect {
|
||||||
viewModel.onBackClick = onBackClick
|
viewModel.onBackClick = onBackClick
|
||||||
@ -80,27 +76,8 @@ fun AppInfoScreen(
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
Column(
|
AppInfo(viewModel.appInfo) {
|
||||||
modifier = Modifier
|
Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
AppIcon(
|
|
||||||
viewModel.appInfo,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(100.dp)
|
|
||||||
.padding(bottom = 5.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
AppLabel(
|
|
||||||
viewModel.appInfo,
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
defaultText = null
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(viewModel.installedApp.version, style = MaterialTheme.typography.bodySmall)
|
|
||||||
|
|
||||||
if (viewModel.installedApp.installType == InstallType.ROOT) {
|
if (viewModel.installedApp.installType == InstallType.ROOT) {
|
||||||
Text(
|
Text(
|
||||||
@ -166,38 +143,35 @@ fun AppInfoScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = 16.dp)
|
modifier = Modifier.padding(vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable { },
|
modifier = Modifier.clickable { },
|
||||||
headlineContent = { Text(stringResource(R.string.applied_patches)) },
|
headlineContent = stringResource(R.string.applied_patches),
|
||||||
supportingContent = {
|
supportingContent =
|
||||||
Text(
|
|
||||||
(viewModel.appliedPatches?.values?.sumOf { it.size } ?: 0).let {
|
(viewModel.appliedPatches?.values?.sumOf { it.size } ?: 0).let {
|
||||||
pluralStringResource(
|
pluralStringResource(
|
||||||
id = R.plurals.applied_patches,
|
id = R.plurals.applied_patches,
|
||||||
it,
|
it,
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
|
||||||
},
|
|
||||||
trailingContent = { Icon(Icons.Filled.ArrowRight, contentDescription = stringResource(R.string.view_applied_patches)) }
|
trailingContent = { Icon(Icons.Filled.ArrowRight, contentDescription = stringResource(R.string.view_applied_patches)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.package_name)) },
|
headlineContent = stringResource(R.string.package_name),
|
||||||
supportingContent = { Text(viewModel.installedApp.currentPackageName) }
|
supportingContent = viewModel.installedApp.currentPackageName
|
||||||
)
|
)
|
||||||
|
|
||||||
if (viewModel.installedApp.originalPackageName != viewModel.installedApp.currentPackageName) {
|
if (viewModel.installedApp.originalPackageName != viewModel.installedApp.currentPackageName) {
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.original_package_name)) },
|
headlineContent = stringResource(R.string.original_package_name),
|
||||||
supportingContent = { Text(viewModel.installedApp.originalPackageName) }
|
supportingContent = viewModel.installedApp.originalPackageName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.install_type)) },
|
headlineContent = stringResource(R.string.install_type),
|
||||||
supportingContent = { Text(stringResource(viewModel.installedApp.installType.stringResource)) }
|
supportingContent = stringResource(viewModel.installedApp.installType.stringResource)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2,10 +2,13 @@ package app.revanced.manager.ui.screen
|
|||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.WarningAmber
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -18,9 +21,11 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||||
|
import app.revanced.manager.patcher.aapt.Aapt
|
||||||
import app.revanced.manager.ui.component.AppIcon
|
import app.revanced.manager.ui.component.AppIcon
|
||||||
import app.revanced.manager.ui.component.AppLabel
|
import app.revanced.manager.ui.component.AppLabel
|
||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
|
import app.revanced.manager.ui.component.NotificationCard
|
||||||
import app.revanced.manager.ui.viewmodel.InstalledAppsViewModel
|
import app.revanced.manager.ui.viewmodel.InstalledAppsViewModel
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
@ -31,43 +36,55 @@ fun InstalledAppsScreen(
|
|||||||
) {
|
) {
|
||||||
val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null)
|
val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null)
|
||||||
|
|
||||||
LazyColumn(
|
Column {
|
||||||
modifier = Modifier.fillMaxSize(),
|
if (!Aapt.supportsDevice()) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
NotificationCard(
|
||||||
verticalArrangement = installedApps?.let { if (it.isEmpty()) Arrangement.Center else Arrangement.Top } ?: Arrangement.Center
|
isWarning = true,
|
||||||
) {
|
icon = Icons.Outlined.WarningAmber,
|
||||||
installedApps?.let { installedApps ->
|
text = stringResource(
|
||||||
|
R.string.unsupported_architecture_warning
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (installedApps.isNotEmpty()) {
|
LazyColumn(
|
||||||
items(
|
modifier = Modifier.fillMaxSize(),
|
||||||
installedApps,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
key = { it.currentPackageName }
|
verticalArrangement = if (installedApps.isNullOrEmpty()) Arrangement.Center else Arrangement.Top
|
||||||
) { installedApp ->
|
) {
|
||||||
viewModel.packageInfoMap[installedApp.currentPackageName].let { packageInfo ->
|
installedApps?.let { installedApps ->
|
||||||
ListItem(
|
|
||||||
modifier = Modifier.clickable { onAppClick(installedApp) },
|
|
||||||
leadingContent = {
|
|
||||||
AppIcon(
|
|
||||||
packageInfo,
|
|
||||||
contentDescription = null,
|
|
||||||
Modifier.size(36.dp)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { AppLabel(packageInfo, defaultText = null) },
|
|
||||||
supportingContent = { Text(installedApp.currentPackageName) }
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if (installedApps.isNotEmpty()) {
|
||||||
|
items(
|
||||||
|
installedApps,
|
||||||
|
key = { it.currentPackageName }
|
||||||
|
) { installedApp ->
|
||||||
|
viewModel.packageInfoMap[installedApp.currentPackageName].let { packageInfo ->
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable { onAppClick(installedApp) },
|
||||||
|
leadingContent = {
|
||||||
|
AppIcon(
|
||||||
|
packageInfo,
|
||||||
|
contentDescription = null,
|
||||||
|
Modifier.size(36.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { AppLabel(packageInfo, defaultText = null) },
|
||||||
|
supportingContent = { Text(installedApp.currentPackageName) }
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.no_patched_apps_found),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
item {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.no_patched_apps_found),
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} ?: item { LoadingIndicator() }
|
} ?: item { LoadingIndicator() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,7 +13,9 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Cancel
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
import androidx.compose.material.icons.outlined.FileDownload
|
||||||
|
import androidx.compose.material.icons.outlined.PostAdd
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
@ -36,8 +38,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
import app.revanced.manager.patcher.worker.Step
|
|
||||||
import app.revanced.manager.patcher.worker.State
|
import app.revanced.manager.patcher.worker.State
|
||||||
|
import app.revanced.manager.patcher.worker.Step
|
||||||
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.ArrowButton
|
import app.revanced.manager.ui.component.ArrowButton
|
||||||
@ -59,7 +61,6 @@ fun InstallerScreen(
|
|||||||
val patcherState by vm.patcherState.observeAsState(null)
|
val patcherState by vm.patcherState.observeAsState(null)
|
||||||
val steps by vm.progress.collectAsStateWithLifecycle()
|
val steps by vm.progress.collectAsStateWithLifecycle()
|
||||||
val canInstall by remember { derivedStateOf { patcherState == true && (vm.installedPackageName != null || !vm.isInstalling) } }
|
val canInstall by remember { derivedStateOf { patcherState == true && (vm.installedPackageName != null || !vm.isInstalling) } }
|
||||||
var dropdownActive by rememberSaveable { mutableStateOf(false) }
|
|
||||||
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
if (showInstallPicker)
|
if (showInstallPicker)
|
||||||
@ -72,23 +73,40 @@ fun InstallerScreen(
|
|||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
title = stringResource(R.string.installer),
|
title = stringResource(R.string.installer),
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick
|
||||||
actions = {
|
|
||||||
IconButton(onClick = { dropdownActive = true }) {
|
|
||||||
Icon(Icons.Outlined.MoreVert, stringResource(R.string.more))
|
|
||||||
}
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = dropdownActive,
|
|
||||||
onDismissRequest = { dropdownActive = false }
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.save_logs)) },
|
|
||||||
onClick = { vm.exportLogs(context) },
|
|
||||||
enabled = patcherState != null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
AnimatedVisibility(patcherState != null) {
|
||||||
|
BottomAppBar(
|
||||||
|
actions = {
|
||||||
|
if (canInstall) {
|
||||||
|
IconButton(onClick = { exportApkLauncher.launch("${vm.packageName}.apk") }) {
|
||||||
|
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(onClick = { vm.exportLogs(context) }) {
|
||||||
|
Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
if (canInstall) {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
text = { Text(stringResource(vm.appButtonText)) },
|
||||||
|
icon = { Icon(Icons.Outlined.FileDownload, stringResource(id = R.string.install_app)) },
|
||||||
|
containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
|
||||||
|
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
|
||||||
|
onClick = {
|
||||||
|
if (vm.installedPackageName == null)
|
||||||
|
showInstallPicker = true
|
||||||
|
else
|
||||||
|
vm.open()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
Column(
|
||||||
@ -100,33 +118,6 @@ fun InstallerScreen(
|
|||||||
steps.forEach {
|
steps.forEach {
|
||||||
InstallStep(it)
|
InstallStep(it)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.Bottom,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
|
|
||||||
enabled = canInstall
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.export_app))
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
if (vm.installedPackageName == null)
|
|
||||||
showInstallPicker = true
|
|
||||||
else
|
|
||||||
vm.open()
|
|
||||||
},
|
|
||||||
enabled = canInstall
|
|
||||||
) {
|
|
||||||
Text(stringResource(vm.appButtonText))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,17 +14,16 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Build
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.FilterList
|
||||||
import androidx.compose.material.icons.outlined.HelpOutline
|
import androidx.compose.material.icons.outlined.HelpOutline
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
|
||||||
import androidx.compose.material.icons.outlined.Restore
|
import androidx.compose.material.icons.outlined.Restore
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
import androidx.compose.material.icons.outlined.Search
|
import androidx.compose.material.icons.outlined.Search
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material.icons.outlined.WarningAmber
|
import androidx.compose.material.icons.outlined.WarningAmber
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
@ -32,16 +31,20 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.ScrollableTabRow
|
import androidx.compose.material3.ScrollableTabRow
|
||||||
|
import androidx.compose.material3.SearchBar
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
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
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.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
|
||||||
@ -61,7 +64,6 @@ import app.revanced.manager.ui.component.AppTopBar
|
|||||||
import app.revanced.manager.ui.component.Countdown
|
import app.revanced.manager.ui.component.Countdown
|
||||||
import app.revanced.manager.ui.component.patches.OptionItem
|
import app.revanced.manager.ui.component.patches.OptionItem
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.BaseSelectionMode
|
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
||||||
@ -73,18 +75,75 @@ import org.koin.compose.rememberKoinInject
|
|||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PatchesSelectorScreen(
|
fun PatchesSelectorScreen(
|
||||||
onPatchClick: (PatchesSelection, Options) -> Unit,
|
onSave: (PatchesSelection?, Options) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: PatchesSelectorViewModel
|
vm: PatchesSelectorViewModel
|
||||||
) {
|
) {
|
||||||
val pagerState = rememberPagerState()
|
|
||||||
val composableScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
val pagerState = rememberPagerState(
|
||||||
|
initialPage = 0,
|
||||||
|
initialPageOffsetFraction = 0f
|
||||||
|
) {
|
||||||
|
bundles.size
|
||||||
|
}
|
||||||
|
val composableScope = rememberCoroutineScope()
|
||||||
|
var search: String? by rememberSaveable {
|
||||||
|
mutableStateOf(null)
|
||||||
|
}
|
||||||
|
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val showPatchButton by remember {
|
||||||
|
derivedStateOf { vm.selectionIsValid(bundles) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBottomSheet) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = {
|
||||||
|
showBottomSheet = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.patches_selector_sheet_filter_title),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.patches_selector_sheet_filter_compat_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = vm.filter and SHOW_SUPPORTED != 0,
|
||||||
|
onClick = { vm.toggleFlag(SHOW_SUPPORTED) },
|
||||||
|
label = { Text(stringResource(R.string.supported)) }
|
||||||
|
)
|
||||||
|
|
||||||
|
FilterChip(
|
||||||
|
selected = vm.filter and SHOW_UNIVERSAL != 0,
|
||||||
|
onClick = { vm.toggleFlag(SHOW_UNIVERSAL) },
|
||||||
|
label = { Text(stringResource(R.string.universal)) },
|
||||||
|
)
|
||||||
|
|
||||||
|
FilterChip(
|
||||||
|
selected = vm.filter and SHOW_UNSUPPORTED != 0,
|
||||||
|
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
|
||||||
|
label = { Text(stringResource(R.string.unsupported)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (vm.compatibleVersions.isNotEmpty())
|
if (vm.compatibleVersions.isNotEmpty())
|
||||||
UnsupportedDialog(
|
UnsupportedDialog(
|
||||||
appVersion = vm.input.selectedApp.version,
|
appVersion = vm.appVersion,
|
||||||
supportedVersions = vm.compatibleVersions,
|
supportedVersions = vm.compatibleVersions,
|
||||||
onDismissRequest = vm::dismissDialogs
|
onDismissRequest = vm::dismissDialogs
|
||||||
)
|
)
|
||||||
@ -106,6 +165,112 @@ fun PatchesSelectorScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun LazyListScope.patchList(
|
||||||
|
uid: Int,
|
||||||
|
patches: List<PatchInfo>,
|
||||||
|
filterFlag: Int,
|
||||||
|
supported: Boolean,
|
||||||
|
header: (@Composable () -> Unit)? = null
|
||||||
|
) {
|
||||||
|
if (patches.isNotEmpty() && (vm.filter and filterFlag) != 0 || vm.filter == 0) {
|
||||||
|
header?.let {
|
||||||
|
item {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(
|
||||||
|
items = patches,
|
||||||
|
key = { it.name }
|
||||||
|
) { patch ->
|
||||||
|
PatchItem(
|
||||||
|
patch = patch,
|
||||||
|
onOptionsDialog = {
|
||||||
|
vm.optionsDialog = uid to patch
|
||||||
|
},
|
||||||
|
selected = supported && vm.isSelected(
|
||||||
|
uid,
|
||||||
|
patch
|
||||||
|
),
|
||||||
|
onToggle = {
|
||||||
|
if (vm.selectionWarningEnabled) {
|
||||||
|
vm.pendingSelectionAction = {
|
||||||
|
vm.togglePatch(uid, patch)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vm.togglePatch(uid, patch)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
supported = supported
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
search?.let { query ->
|
||||||
|
SearchBar(
|
||||||
|
query = query,
|
||||||
|
onQueryChange = { new ->
|
||||||
|
search = new
|
||||||
|
},
|
||||||
|
onSearch = {},
|
||||||
|
active = true,
|
||||||
|
onActiveChange = { new ->
|
||||||
|
if (new) return@SearchBar
|
||||||
|
search = null
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(stringResource(R.string.search_patches))
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
IconButton(onClick = { search = null }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ArrowBack,
|
||||||
|
stringResource(R.string.back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val bundle = bundles[pagerState.currentPage]
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
fun List<PatchInfo>.searched() = filter {
|
||||||
|
it.name.contains(query, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
patchList(
|
||||||
|
uid = bundle.uid,
|
||||||
|
patches = bundle.supported.searched(),
|
||||||
|
filterFlag = SHOW_SUPPORTED,
|
||||||
|
supported = true
|
||||||
|
)
|
||||||
|
patchList(
|
||||||
|
uid = bundle.uid,
|
||||||
|
patches = bundle.universal.searched(),
|
||||||
|
filterFlag = SHOW_UNIVERSAL,
|
||||||
|
supported = true
|
||||||
|
) {
|
||||||
|
ListHeader(
|
||||||
|
title = stringResource(R.string.universal_patches),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vm.allowExperimental) return@LazyColumn
|
||||||
|
patchList(
|
||||||
|
uid = bundle.uid,
|
||||||
|
patches = bundle.unsupported.searched(),
|
||||||
|
filterFlag = SHOW_UNSUPPORTED,
|
||||||
|
supported = true
|
||||||
|
) {
|
||||||
|
ListHeader(
|
||||||
|
title = stringResource(R.string.unsupported_patches),
|
||||||
|
onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
@ -115,50 +280,28 @@ fun PatchesSelectorScreen(
|
|||||||
IconButton(onClick = vm::reset) {
|
IconButton(onClick = vm::reset) {
|
||||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||||
}
|
}
|
||||||
|
IconButton(onClick = { showBottomSheet = true }) {
|
||||||
var dropdownActive by rememberSaveable {
|
Icon(Icons.Outlined.FilterList, stringResource(R.string.more))
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
}
|
||||||
// This part should probably be changed
|
IconButton(
|
||||||
IconButton(onClick = { dropdownActive = true }) {
|
onClick = {
|
||||||
Icon(Icons.Outlined.MoreVert, stringResource(R.string.more))
|
search = ""
|
||||||
DropdownMenu(
|
|
||||||
expanded = dropdownActive,
|
|
||||||
onDismissRequest = { dropdownActive = false }
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
val id =
|
|
||||||
if (vm.baseSelectionMode == BaseSelectionMode.DEFAULT)
|
|
||||||
R.string.menu_opt_selection_mode_previous else R.string.menu_opt_selection_mode_default
|
|
||||||
|
|
||||||
Text(stringResource(id))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
dropdownActive = false
|
|
||||||
vm.switchBaseSelectionMode()
|
|
||||||
},
|
|
||||||
enabled = vm.hasPreviousSelection
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
) {
|
||||||
IconButton(onClick = { }) {
|
|
||||||
Icon(Icons.Outlined.Search, stringResource(R.string.search))
|
Icon(Icons.Outlined.Search, stringResource(R.string.search))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
|
if (!showPatchButton) return@Scaffold
|
||||||
|
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = { Text(stringResource(R.string.patch)) },
|
text = { Text(stringResource(R.string.save)) },
|
||||||
icon = { Icon(Icons.Default.Build, null) },
|
icon = { Icon(Icons.Outlined.Save, null) },
|
||||||
onClick = {
|
onClick = {
|
||||||
// TODO: only allow this if all required options have been set.
|
// TODO: only allow this if all required options have been set.
|
||||||
composableScope.launch {
|
onSave(vm.getCustomSelection(), vm.getOptions())
|
||||||
val selection = vm.getSelection()
|
|
||||||
vm.saveSelection(selection).join()
|
|
||||||
onPatchClick(selection, vm.getOptions())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -192,113 +335,40 @@ fun PatchesSelectorScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
pageCount = bundles.size,
|
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
userScrollEnabled = true,
|
userScrollEnabled = true,
|
||||||
pageContent = { index ->
|
pageContent = { index ->
|
||||||
val bundle = bundles[index]
|
val bundle = bundles[index]
|
||||||
|
|
||||||
Column {
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
Row(
|
) {
|
||||||
modifier = Modifier
|
patchList(
|
||||||
.fillMaxWidth()
|
uid = bundle.uid,
|
||||||
.padding(horizontal = 10.dp, vertical = 2.dp),
|
patches = bundle.supported,
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
filterFlag = SHOW_SUPPORTED,
|
||||||
|
supported = true
|
||||||
|
)
|
||||||
|
patchList(
|
||||||
|
uid = bundle.uid,
|
||||||
|
patches = bundle.universal,
|
||||||
|
filterFlag = SHOW_UNIVERSAL,
|
||||||
|
supported = true
|
||||||
) {
|
) {
|
||||||
FilterChip(
|
ListHeader(
|
||||||
selected = vm.filter and SHOW_SUPPORTED != 0 && bundle.supported.isNotEmpty(),
|
title = stringResource(R.string.universal_patches),
|
||||||
onClick = { vm.toggleFlag(SHOW_SUPPORTED) },
|
|
||||||
label = { Text(stringResource(R.string.supported)) },
|
|
||||||
enabled = bundle.supported.isNotEmpty()
|
|
||||||
)
|
|
||||||
|
|
||||||
FilterChip(
|
|
||||||
selected = vm.filter and SHOW_UNIVERSAL != 0 && bundle.universal.isNotEmpty(),
|
|
||||||
onClick = { vm.toggleFlag(SHOW_UNIVERSAL) },
|
|
||||||
label = { Text(stringResource(R.string.universal)) },
|
|
||||||
enabled = bundle.universal.isNotEmpty()
|
|
||||||
)
|
|
||||||
|
|
||||||
FilterChip(
|
|
||||||
selected = vm.filter and SHOW_UNSUPPORTED != 0 && bundle.unsupported.isNotEmpty(),
|
|
||||||
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
|
|
||||||
label = { Text(stringResource(R.string.unsupported)) },
|
|
||||||
enabled = bundle.unsupported.isNotEmpty()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
patchList(
|
||||||
val allowExperimental by vm.allowExperimental.getAsState()
|
uid = bundle.uid,
|
||||||
|
patches = bundle.unsupported,
|
||||||
LazyColumn(
|
filterFlag = SHOW_UNSUPPORTED,
|
||||||
modifier = Modifier.fillMaxSize()
|
supported = vm.allowExperimental
|
||||||
) {
|
) {
|
||||||
fun LazyListScope.patchList(
|
ListHeader(
|
||||||
patches: List<PatchInfo>,
|
title = stringResource(R.string.unsupported_patches),
|
||||||
filterFlag: Int,
|
onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) }
|
||||||
supported: Boolean,
|
|
||||||
header: (@Composable () -> Unit)? = null
|
|
||||||
) {
|
|
||||||
if (patches.isNotEmpty() && (vm.filter and filterFlag) != 0 || vm.filter == 0) {
|
|
||||||
header?.let {
|
|
||||||
item {
|
|
||||||
it()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items(
|
|
||||||
items = patches,
|
|
||||||
key = { it.name }
|
|
||||||
) { patch ->
|
|
||||||
PatchItem(
|
|
||||||
patch = patch,
|
|
||||||
onOptionsDialog = {
|
|
||||||
vm.optionsDialog = bundle.uid to patch
|
|
||||||
},
|
|
||||||
selected = supported && vm.isSelected(
|
|
||||||
bundle.uid,
|
|
||||||
patch
|
|
||||||
),
|
|
||||||
onToggle = {
|
|
||||||
if (vm.selectionWarningEnabled) {
|
|
||||||
vm.pendingSelectionAction = {
|
|
||||||
vm.togglePatch(bundle.uid, patch)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
vm.togglePatch(bundle.uid, patch)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
supported = supported
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patchList(
|
|
||||||
patches = bundle.supported,
|
|
||||||
filterFlag = SHOW_SUPPORTED,
|
|
||||||
supported = true
|
|
||||||
)
|
)
|
||||||
patchList(
|
|
||||||
patches = bundle.universal,
|
|
||||||
filterFlag = SHOW_UNIVERSAL,
|
|
||||||
supported = true
|
|
||||||
) {
|
|
||||||
ListHeader(
|
|
||||||
title = stringResource(R.string.universal_patches),
|
|
||||||
onHelpClick = { }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
patchList(
|
|
||||||
patches = bundle.unsupported,
|
|
||||||
filterFlag = SHOW_UNSUPPORTED,
|
|
||||||
supported = allowExperimental
|
|
||||||
) {
|
|
||||||
ListHeader(
|
|
||||||
title = stringResource(R.string.unsupported_patches),
|
|
||||||
onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -400,7 +470,7 @@ fun PatchItem(
|
|||||||
leadingContent = {
|
leadingContent = {
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = selected,
|
checked = selected,
|
||||||
onCheckedChange = null,
|
onCheckedChange = { onToggle() },
|
||||||
enabled = supported
|
enabled = supported
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -501,7 +571,7 @@ fun OptionsDialog(
|
|||||||
items(patch.options, key = { it.key }) { option ->
|
items(patch.options, key = { it.key }) { option ->
|
||||||
val key = option.key
|
val key = option.key
|
||||||
val value =
|
val value =
|
||||||
if (values == null || !values.contains(key)) option.defaultValue else values[key]
|
if (values == null || !values.contains(key)) option.default else values[key]
|
||||||
|
|
||||||
OptionItem(option = option, value = value, setValue = { set(key, it) })
|
OptionItem(option = option, value = value, setValue = { set(key, it) })
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,229 @@
|
|||||||
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowRight
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.ui.component.AppInfo
|
||||||
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.destination.SelectedAppInfoDestination
|
||||||
|
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
||||||
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||||
|
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||||
|
import app.revanced.manager.util.Options
|
||||||
|
import app.revanced.manager.util.PatchesSelection
|
||||||
|
import app.revanced.manager.util.toast
|
||||||
|
import dev.olshevski.navigation.reimagined.AnimatedNavHost
|
||||||
|
import dev.olshevski.navigation.reimagined.NavBackHandler
|
||||||
|
import dev.olshevski.navigation.reimagined.navigate
|
||||||
|
import dev.olshevski.navigation.reimagined.pop
|
||||||
|
import dev.olshevski.navigation.reimagined.rememberNavController
|
||||||
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectedAppInfoScreen(
|
||||||
|
onPatchClick: (SelectedApp, PatchesSelection, Options) -> Unit,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
vm: SelectedAppInfoViewModel
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val packageName = vm.selectedApp.packageName
|
||||||
|
val version = vm.selectedApp.version
|
||||||
|
val bundles by remember(packageName, version) {
|
||||||
|
vm.bundlesRepo.bundleInfoFlow(packageName, version)
|
||||||
|
}.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
|
||||||
|
val allowExperimental by vm.prefs.allowExperimental.getAsState()
|
||||||
|
val patches by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
vm.getPatches(bundles, allowExperimental)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val selectedPatchCount by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
patches.values.sumOf { it.size }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val availablePatchCount by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
bundles.sumOf { it.patchCount }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val navController =
|
||||||
|
rememberNavController<SelectedAppInfoDestination>(startDestination = SelectedAppInfoDestination.Main)
|
||||||
|
|
||||||
|
NavBackHandler(controller = navController)
|
||||||
|
|
||||||
|
AnimatedNavHost(controller = navController) { destination ->
|
||||||
|
when (destination) {
|
||||||
|
is SelectedAppInfoDestination.Main -> SelectedAppInfoScreen(
|
||||||
|
onPatchClick = patchClick@{
|
||||||
|
if (selectedPatchCount == 0) {
|
||||||
|
context.toast(context.getString(R.string.no_patches_selected))
|
||||||
|
|
||||||
|
return@patchClick
|
||||||
|
}
|
||||||
|
onPatchClick(
|
||||||
|
vm.selectedApp,
|
||||||
|
patches,
|
||||||
|
vm.getOptionsFiltered(bundles)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onPatchSelectorClick = {
|
||||||
|
navController.navigate(
|
||||||
|
SelectedAppInfoDestination.PatchesSelector(
|
||||||
|
vm.selectedApp,
|
||||||
|
vm.getCustomPatches(
|
||||||
|
bundles,
|
||||||
|
allowExperimental
|
||||||
|
),
|
||||||
|
vm.options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onVersionSelectorClick = {
|
||||||
|
navController.navigate(SelectedAppInfoDestination.VersionSelector)
|
||||||
|
},
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
availablePatchCount = availablePatchCount,
|
||||||
|
selectedPatchCount = selectedPatchCount,
|
||||||
|
packageName = packageName,
|
||||||
|
version = version,
|
||||||
|
packageInfo = vm.selectedAppInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
is SelectedAppInfoDestination.VersionSelector -> VersionSelectorScreen(
|
||||||
|
onBackClick = navController::pop,
|
||||||
|
onAppClick = {
|
||||||
|
vm.selectedApp = it
|
||||||
|
navController.pop()
|
||||||
|
},
|
||||||
|
viewModel = getViewModel { parametersOf(packageName) }
|
||||||
|
)
|
||||||
|
|
||||||
|
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
|
||||||
|
onSave = { patches, options ->
|
||||||
|
vm.updateConfiguration(patches, options, bundles)
|
||||||
|
navController.pop()
|
||||||
|
},
|
||||||
|
onBackClick = navController::pop,
|
||||||
|
vm = getViewModel {
|
||||||
|
parametersOf(
|
||||||
|
PatchesSelectorViewModel.Params(
|
||||||
|
destination.app,
|
||||||
|
destination.currentSelection,
|
||||||
|
destination.options,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun SelectedAppInfoScreen(
|
||||||
|
onPatchClick: () -> Unit,
|
||||||
|
onPatchSelectorClick: () -> Unit,
|
||||||
|
onVersionSelectorClick: () -> Unit,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
availablePatchCount: Int,
|
||||||
|
selectedPatchCount: Int,
|
||||||
|
packageName: String,
|
||||||
|
version: String,
|
||||||
|
packageInfo: PackageInfo?,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
AppTopBar(
|
||||||
|
title = stringResource(R.string.app_info),
|
||||||
|
onBackClick = onBackClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
AppInfo(packageInfo, placeholderLabel = packageName) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.selected_app_meta, version, availablePatchCount),
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PageItem(R.string.patch, stringResource(R.string.patch_item_description), onPatchClick)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.advanced),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
PageItem(
|
||||||
|
R.string.patch_selector_item,
|
||||||
|
stringResource(R.string.patch_selector_item_description, selectedPatchCount),
|
||||||
|
onPatchSelectorClick
|
||||||
|
)
|
||||||
|
PageItem(
|
||||||
|
R.string.version_selector_item,
|
||||||
|
stringResource(R.string.version_selector_item_description, version),
|
||||||
|
onVersionSelectorClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(start = 8.dp),
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(title),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
description,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Icon(Icons.Outlined.ArrowRight, null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
@ -7,18 +7,11 @@ import android.net.Uri
|
|||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.BatteryAlert
|
import androidx.compose.material.icons.filled.BatteryAlert
|
||||||
@ -29,32 +22,39 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.NotificationCard
|
||||||
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.destination.SettingsDestination
|
import app.revanced.manager.ui.destination.SettingsDestination
|
||||||
import app.revanced.manager.ui.screen.settings.*
|
import app.revanced.manager.ui.screen.settings.*
|
||||||
import app.revanced.manager.ui.screen.settings.update.ManagerUpdateChangelog
|
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
|
||||||
import app.revanced.manager.ui.screen.settings.update.UpdateProgressScreen
|
import app.revanced.manager.ui.screen.settings.update.UpdateScreen
|
||||||
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
|
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
|
||||||
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
||||||
import dev.olshevski.navigation.reimagined.*
|
import dev.olshevski.navigation.reimagined.*
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
import org.koin.androidx.compose.getViewModel as getComposeViewModel
|
||||||
|
|
||||||
@SuppressLint("BatteryLife")
|
@SuppressLint("BatteryLife")
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
|
startDestination: SettingsDestination,
|
||||||
viewModel: SettingsViewModel = getViewModel()
|
viewModel: SettingsViewModel = getViewModel()
|
||||||
) {
|
) {
|
||||||
val navController =
|
val navController = rememberNavController(startDestination)
|
||||||
rememberNavController<SettingsDestination>(startDestination = SettingsDestination.Settings)
|
|
||||||
|
val backClick: () -> Unit = {
|
||||||
|
if (navController.backstack.entries.size == 1)
|
||||||
|
onBackClick()
|
||||||
|
else navController.pop()
|
||||||
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
@ -98,50 +98,54 @@ fun SettingsScreen(
|
|||||||
controller = navController
|
controller = navController
|
||||||
) { destination ->
|
) { destination ->
|
||||||
when (destination) {
|
when (destination) {
|
||||||
|
|
||||||
is SettingsDestination.General -> GeneralSettingsScreen(
|
is SettingsDestination.General -> GeneralSettingsScreen(
|
||||||
onBackClick = { navController.pop() },
|
onBackClick = backClick,
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsDestination.Advanced -> AdvancedSettingsScreen(
|
is SettingsDestination.Advanced -> AdvancedSettingsScreen(
|
||||||
onBackClick = { navController.pop() }
|
onBackClick = backClick
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsDestination.Updates -> UpdatesSettingsScreen(
|
is SettingsDestination.Updates -> UpdatesSettingsScreen(
|
||||||
onBackClick = { navController.pop() },
|
onBackClick = backClick,
|
||||||
onChangelogClick = { navController.navigate(SettingsDestination.UpdateChangelog) },
|
onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) },
|
||||||
onUpdateClick = { navController.navigate(SettingsDestination.UpdateProgress) }
|
onUpdateClick = { navController.navigate(SettingsDestination.Update(false)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsDestination.Downloads -> DownloadsSettingsScreen(
|
is SettingsDestination.Downloads -> DownloadsSettingsScreen(
|
||||||
onBackClick = { navController.pop() }
|
onBackClick = backClick
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsDestination.ImportExport -> ImportExportSettingsScreen(
|
is SettingsDestination.ImportExport -> ImportExportSettingsScreen(
|
||||||
onBackClick = { navController.pop() }
|
onBackClick = backClick
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsDestination.About -> AboutSettingsScreen(
|
is SettingsDestination.About -> AboutSettingsScreen(
|
||||||
onBackClick = { navController.pop() },
|
onBackClick = backClick,
|
||||||
onContributorsClick = { navController.navigate(SettingsDestination.Contributors) },
|
onContributorsClick = { navController.navigate(SettingsDestination.Contributors) },
|
||||||
onLicensesClick = { navController.navigate(SettingsDestination.Licenses) }
|
onLicensesClick = { navController.navigate(SettingsDestination.Licenses) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsDestination.UpdateProgress -> UpdateProgressScreen(
|
is SettingsDestination.Update -> UpdateScreen(
|
||||||
onBackClick = { navController.pop() },
|
onBackClick = backClick,
|
||||||
|
vm = getComposeViewModel {
|
||||||
|
parametersOf(
|
||||||
|
destination.downloadOnScreenEntry
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsDestination.UpdateChangelog -> ManagerUpdateChangelog(
|
is SettingsDestination.Changelogs -> ChangelogsScreen(
|
||||||
onBackClick = { navController.pop() },
|
onBackClick = backClick,
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsDestination.Contributors -> ContributorScreen(
|
is SettingsDestination.Contributors -> ContributorScreen(
|
||||||
onBackClick = { navController.pop() },
|
onBackClick = backClick,
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsDestination.Licenses -> LicensesScreen(
|
is SettingsDestination.Licenses -> LicensesScreen(
|
||||||
onBackClick = { navController.pop() },
|
onBackClick = backClick,
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsDestination.Settings -> {
|
is SettingsDestination.Settings -> {
|
||||||
@ -149,7 +153,7 @@ fun SettingsScreen(
|
|||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
title = stringResource(R.string.settings),
|
title = stringResource(R.string.settings),
|
||||||
onBackClick = onBackClick,
|
onBackClick = backClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@ -157,61 +161,27 @@ fun SettingsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState())
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(visible = showBatteryButton) {
|
AnimatedVisibility(visible = showBatteryButton) {
|
||||||
Card(
|
NotificationCard(
|
||||||
onClick = {
|
isWarning = true,
|
||||||
|
icon = Icons.Default.BatteryAlert,
|
||||||
|
text = stringResource(R.string.battery_optimization_notification),
|
||||||
|
primaryAction = {
|
||||||
context.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
context.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||||
data = Uri.parse("package:${context.packageName}")
|
data = Uri.parse("package:${context.packageName}")
|
||||||
})
|
})
|
||||||
showBatteryButton =
|
showBatteryButton =
|
||||||
!pm.isIgnoringBatteryOptimizations(context.packageName)
|
!pm.isIgnoringBatteryOptimizations(context.packageName)
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
.clip(RoundedCornerShape(24.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.tertiaryContainer),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.BatteryAlert,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.onTertiaryContainer,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.battery_optimization_notification),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
settingsSections.forEach { (titleDescIcon, destination) ->
|
settingsSections.forEach { (titleDescIcon, destination) ->
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable { navController.navigate(destination) },
|
modifier = Modifier.clickable { navController.navigate(destination) },
|
||||||
headlineContent = {
|
headlineContent = stringResource(titleDescIcon.first),
|
||||||
Text(
|
supportingContent = stringResource(titleDescIcon.second),
|
||||||
stringResource(titleDescIcon.first),
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
},
|
|
||||||
supportingContent = {
|
|
||||||
Text(
|
|
||||||
stringResource(titleDescIcon.second),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
},
|
|
||||||
leadingContent = { Icon(titleDescIcon.third, null) }
|
leadingContent = { Icon(titleDescIcon.third, null) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -220,4 +190,4 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,9 +1,18 @@
|
|||||||
package app.revanced.manager.ui.screen.settings
|
package app.revanced.manager.ui.screen.settings
|
||||||
|
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@ -11,25 +20,30 @@ import androidx.compose.material.icons.outlined.Code
|
|||||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||||
import androidx.compose.material.icons.outlined.Language
|
import androidx.compose.material.icons.outlined.Language
|
||||||
import androidx.compose.material.icons.outlined.MailOutline
|
import androidx.compose.material.icons.outlined.MailOutline
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
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.BuildConfig
|
import app.revanced.manager.BuildConfig
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.destination.SettingsDestination
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
|
import app.revanced.manager.util.isDebuggable
|
||||||
import app.revanced.manager.util.openUrl
|
import app.revanced.manager.util.openUrl
|
||||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||||
import dev.olshevski.navigation.reimagined.NavController
|
|
||||||
import dev.olshevski.navigation.reimagined.navigate
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AboutSettingsScreen(
|
fun AboutSettingsScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
@ -37,7 +51,10 @@ fun AboutSettingsScreen(
|
|||||||
onLicensesClick: () -> Unit,
|
onLicensesClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val icon = painterResource(R.drawable.ic_logo_ring)
|
// painterResource() is broken on release builds for some reason.
|
||||||
|
val icon = rememberDrawablePainter(drawable = remember {
|
||||||
|
AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring)
|
||||||
|
})
|
||||||
|
|
||||||
val filledButton = listOf(
|
val filledButton = listOf(
|
||||||
Triple(Icons.Outlined.FavoriteBorder, stringResource(R.string.donate)) {
|
Triple(Icons.Outlined.FavoriteBorder, stringResource(R.string.donate)) {
|
||||||
@ -57,17 +74,25 @@ fun AboutSettingsScreen(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
val listItems = listOf(
|
val listItems = listOfNotNull(
|
||||||
Triple(stringResource(R.string.submit_feedback), stringResource(R.string.submit_feedback_description),
|
Triple(stringResource(R.string.submit_feedback),
|
||||||
|
stringResource(R.string.submit_feedback_description),
|
||||||
third = {
|
third = {
|
||||||
context.openUrl("https://github.com/ReVanced/revanced-manager/issues/new/choose")
|
context.openUrl("https://github.com/ReVanced/revanced-manager/issues/new/choose")
|
||||||
}),
|
}),
|
||||||
Triple(stringResource(R.string.contributors), stringResource(R.string.contributors_description),
|
Triple(
|
||||||
third = onContributorsClick),
|
stringResource(R.string.contributors),
|
||||||
Triple(stringResource(R.string.developer_options), stringResource(R.string.developer_options_description),
|
stringResource(R.string.contributors_description),
|
||||||
third = { /*TODO*/ }),
|
third = onContributorsClick
|
||||||
Triple(stringResource(R.string.opensource_licenses), stringResource(R.string.opensource_licenses_description),
|
),
|
||||||
third = onLicensesClick)
|
Triple(stringResource(R.string.developer_options),
|
||||||
|
stringResource(R.string.developer_options_description),
|
||||||
|
third = { /*TODO*/ }).takeIf { context.isDebuggable },
|
||||||
|
Triple(
|
||||||
|
stringResource(R.string.opensource_licenses),
|
||||||
|
stringResource(R.string.opensource_licenses_description),
|
||||||
|
third = onLicensesClick
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -82,96 +107,87 @@ fun AboutSettingsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState()),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
|
painter = icon,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
Image(painter = icon, contentDescription = null)
|
Text(
|
||||||
Text(stringResource(R.string.app_name), style = MaterialTheme.typography.titleLarge)
|
stringResource(R.string.app_name),
|
||||||
Text( text = stringResource(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")", style = MaterialTheme.typography.bodyMedium)
|
style = MaterialTheme.typography.headlineSmall
|
||||||
Row(
|
)
|
||||||
modifier = Modifier.padding(top = 12.dp)
|
Text(
|
||||||
) {
|
text = stringResource(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")",
|
||||||
filledButton.forEach { (icon, text, onClick) ->
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
FilledTonalButton(
|
color = MaterialTheme.colorScheme.outline
|
||||||
onClick = onClick,
|
)
|
||||||
modifier = Modifier.padding(end = 8.dp)
|
}
|
||||||
|
FlowRow(
|
||||||
|
maxItemsInEachRow = 2,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
filledButton.forEach { (icon, text, onClick) ->
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(
|
Icon(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
icon,
|
||||||
) {
|
contentDescription = null,
|
||||||
Icon(
|
modifier = Modifier.size(18.dp)
|
||||||
icon,
|
)
|
||||||
contentDescription = null,
|
Text(
|
||||||
modifier = Modifier
|
text,
|
||||||
.size(28.dp)
|
style = MaterialTheme.typography.labelLarge
|
||||||
.padding(end = 8.dp),
|
)
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
outlinedButton.forEach { (icon, text, onClick) ->
|
||||||
Row(
|
OutlinedButton(
|
||||||
modifier = Modifier.padding(top = 12.dp)
|
onClick = onClick
|
||||||
) {
|
) {
|
||||||
outlinedButton.forEach { (icon, text, onClick) ->
|
Row(
|
||||||
Button(
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
onClick = onClick,
|
verticalAlignment = Alignment.CenterVertically
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
),
|
|
||||||
border = ButtonDefaults.outlinedButtonBorder
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Icon(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
icon,
|
||||||
) {
|
contentDescription = null,
|
||||||
Icon(
|
modifier = Modifier.size(18.dp)
|
||||||
icon,
|
)
|
||||||
contentDescription = null,
|
Text(
|
||||||
modifier = Modifier
|
text,
|
||||||
.size(28.dp)
|
style = MaterialTheme.typography.labelLarge,
|
||||||
.padding(end = 8.dp),
|
color = MaterialTheme.colorScheme.primary
|
||||||
tint = MaterialTheme.colorScheme.primary
|
)
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
OutlinedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||||
.border(
|
|
||||||
width = 1.dp,
|
|
||||||
color = MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
shape = MaterialTheme.shapes.medium
|
|
||||||
)
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
) {
|
||||||
Column {
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.about_revanced_manager),
|
text = stringResource(R.string.about_revanced_manager),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium
|
||||||
modifier = Modifier.padding(bottom = 8.dp),
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.revanced_manager_description),
|
text = stringResource(R.string.revanced_manager_description),
|
||||||
@ -179,18 +195,17 @@ fun AboutSettingsScreen(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Column {
|
||||||
listItems.forEach { (title, description, onClick) ->
|
listItems.forEach { (title, description, onClick) ->
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp)
|
.clickable { onClick() },
|
||||||
.clickable { onClick() },
|
headlineContent = title,
|
||||||
headlineContent = { Text(title, style = MaterialTheme.typography.titleLarge) },
|
supportingContent = description
|
||||||
supportingContent = { Text(description, style = MaterialTheme.typography.bodyMedium,color = MaterialTheme.colorScheme.outline) }
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,6 @@ import androidx.compose.material.icons.outlined.Http
|
|||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@ -36,6 +35,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
import app.revanced.manager.ui.component.GroupHeader
|
||||||
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.component.settings.BooleanItem
|
import app.revanced.manager.ui.component.settings.BooleanItem
|
||||||
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
@ -70,7 +70,7 @@ fun AdvancedSettingsScreen(
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
val apiUrl by vm.apiUrl.getAsState()
|
val apiUrl by vm.prefs.api.getAsState()
|
||||||
var showApiUrlDialog by rememberSaveable { mutableStateOf(false) }
|
var showApiUrlDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
if (showApiUrlDialog) {
|
if (showApiUrlDialog) {
|
||||||
@ -79,9 +79,9 @@ fun AdvancedSettingsScreen(
|
|||||||
it?.let(vm::setApiUrl)
|
it?.let(vm::setApiUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.api_url)) },
|
headlineContent = stringResource(R.string.api_url),
|
||||||
supportingContent = { Text(apiUrl) },
|
supportingContent = apiUrl,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
showApiUrlDialog = true
|
showApiUrlDialog = true
|
||||||
}
|
}
|
||||||
@ -89,42 +89,48 @@ fun AdvancedSettingsScreen(
|
|||||||
|
|
||||||
GroupHeader(stringResource(R.string.patcher))
|
GroupHeader(stringResource(R.string.patcher))
|
||||||
BooleanItem(
|
BooleanItem(
|
||||||
preference = vm.allowExperimental,
|
preference = vm.prefs.allowExperimental,
|
||||||
coroutineScope = vm.viewModelScope,
|
coroutineScope = vm.viewModelScope,
|
||||||
headline = R.string.experimental_patches,
|
headline = R.string.experimental_patches,
|
||||||
description = R.string.experimental_patches_description
|
description = R.string.experimental_patches_description
|
||||||
)
|
)
|
||||||
|
BooleanItem(
|
||||||
|
preference = vm.prefs.multithreadingDexFileWriter,
|
||||||
|
coroutineScope = vm.viewModelScope,
|
||||||
|
headline = R.string.multithreaded_dex_file_writer,
|
||||||
|
description = R.string.multithreaded_dex_file_writer_description,
|
||||||
|
)
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.patch_bundles_section))
|
GroupHeader(stringResource(R.string.patch_bundles_section))
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.patch_bundles_redownload)) },
|
headlineContent = stringResource(R.string.patch_bundles_redownload),
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
vm.redownloadBundles()
|
vm.redownloadBundles()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.patch_bundles_reset)) },
|
headlineContent = stringResource(R.string.patch_bundles_reset),
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
vm.resetBundles()
|
vm.resetBundles()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.device))
|
GroupHeader(stringResource(R.string.device))
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.device_model)) },
|
headlineContent = stringResource(R.string.device_model),
|
||||||
supportingContent = { Text(Build.MODEL) }
|
supportingContent = Build.MODEL
|
||||||
)
|
)
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.device_android_version)) },
|
headlineContent = stringResource(R.string.device_android_version),
|
||||||
supportingContent = { Text(Build.VERSION.RELEASE) }
|
supportingContent = Build.VERSION.RELEASE
|
||||||
)
|
)
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.device_architectures)) },
|
headlineContent = stringResource(R.string.device_architectures),
|
||||||
supportingContent = { Text(Build.SUPPORTED_ABIS.joinToString(", ")) }
|
supportingContent = Build.SUPPORTED_ABIS.joinToString(", ")
|
||||||
)
|
)
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.device_memory_limit)) },
|
headlineContent = stringResource(R.string.device_memory_limit),
|
||||||
supportingContent = { Text(memoryLimit) }
|
supportingContent = memoryLimit
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,39 @@
|
|||||||
package app.revanced.manager.ui.screen.settings
|
package app.revanced.manager.ui.screen.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material.icons.outlined.ArrowDropDown
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material.icons.outlined.ArrowDropUp
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.coerceAtMost
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.times
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.network.dto.ReVancedContributor
|
import app.revanced.manager.network.dto.ReVancedContributor
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.ArrowButton
|
|
||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
import app.revanced.manager.ui.viewmodel.ContributorViewModel
|
import app.revanced.manager.ui.viewmodel.ContributorViewModel
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ContributorScreen(
|
fun ContributorScreen(
|
||||||
@ -45,92 +49,148 @@ fun ContributorScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(),
|
||||||
.verticalScroll(rememberScrollState())
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = if (repositories.isNullOrEmpty()) Arrangement.Center else Arrangement.spacedBy(
|
||||||
|
24.dp
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
if(repositories.isEmpty()) {
|
repositories?.let { repositories ->
|
||||||
LoadingIndicator()
|
if (repositories.isEmpty()) {
|
||||||
}
|
item {
|
||||||
repositories.forEach {
|
Text(
|
||||||
ExpandableListCard(
|
text = stringResource(id = R.string.no_contributors_found),
|
||||||
title = it.name,
|
style = MaterialTheme.typography.titleLarge
|
||||||
contributors = it.contributors
|
)
|
||||||
)
|
}
|
||||||
}
|
} else {
|
||||||
|
items(
|
||||||
|
items = repositories,
|
||||||
|
key = { it.name }
|
||||||
|
) {
|
||||||
|
ContributorsCard(
|
||||||
|
title = it.name,
|
||||||
|
contributors = it.contributors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: item { LoadingIndicator() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ExpandableListCard(
|
fun ContributorsCard(
|
||||||
title: String,
|
title: String,
|
||||||
contributors: List<ReVancedContributor>
|
contributors: List<ReVancedContributor>,
|
||||||
|
itemsPerPage: Int = 12,
|
||||||
|
numberOfRows: Int = 2
|
||||||
) {
|
) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
val itemsPerRow = (itemsPerPage / numberOfRows)
|
||||||
|
|
||||||
|
// Create a list of contributors grouped by itemsPerPage
|
||||||
|
val contributorsByPage = remember(itemsPerPage, contributors) {
|
||||||
|
contributors.chunked(itemsPerPage)
|
||||||
|
}
|
||||||
|
val pagerState = rememberPagerState { contributorsByPage.size }
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
shape = RoundedCornerShape(30.dp),
|
|
||||||
elevation = CardDefaults.outlinedCardElevation(),
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp)
|
|
||||||
.border(
|
.border(
|
||||||
width = 2.dp,
|
width = 1.dp,
|
||||||
color = MaterialTheme.colorScheme.outline,
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium
|
||||||
),
|
),
|
||||||
colors = CardDefaults.outlinedCardColors(),
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer)
|
||||||
) {
|
) {
|
||||||
Column() {
|
Column(
|
||||||
Row() {
|
modifier = Modifier.padding(16.dp),
|
||||||
ListItem(
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
headlineContent = {
|
) {
|
||||||
Text(
|
Row(
|
||||||
text = processHeadlineText(title),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
style = MaterialTheme.typography.titleMedium
|
verticalAlignment = Alignment.CenterVertically
|
||||||
)
|
) {
|
||||||
},
|
Text(
|
||||||
trailingContent = {
|
text = processHeadlineText(title),
|
||||||
if (contributors.isNotEmpty()) {
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Medium)
|
||||||
ArrowButton(
|
)
|
||||||
expanded = expanded,
|
Text(
|
||||||
onClick = { expanded = !expanded }
|
text = "(${(pagerState.currentPage + 1)}/${pagerState.pageCount})",
|
||||||
)
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
}
|
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold)
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (expanded) {
|
HorizontalPager(
|
||||||
FlowRow(
|
state = pagerState,
|
||||||
modifier = Modifier
|
userScrollEnabled = true,
|
||||||
.fillMaxWidth()
|
modifier = Modifier.fillMaxSize(),
|
||||||
.wrapContentHeight()
|
) { page ->
|
||||||
.padding(8.dp),
|
BoxWithConstraints {
|
||||||
) {
|
val spaceBetween = 16.dp
|
||||||
contributors.forEach {
|
val maxWidth = this.maxWidth
|
||||||
AsyncImage(
|
val itemSize = (maxWidth - (itemsPerRow - 1) * spaceBetween) / itemsPerRow
|
||||||
model = it.avatarUrl,
|
val itemSpacing = (maxWidth - itemSize * 6) / (itemsPerRow - 1)
|
||||||
contentDescription = it.avatarUrl,
|
FlowRow(
|
||||||
contentScale = ContentScale.Crop,
|
maxItemsInEachRow = itemsPerRow,
|
||||||
modifier = Modifier
|
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
|
||||||
.padding(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
.size(45.dp)
|
modifier = Modifier.fillMaxWidth()
|
||||||
.clip(CircleShape)
|
) {
|
||||||
)
|
contributorsByPage[page].forEach {
|
||||||
|
if (itemSize > 100.dp) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.width(itemSize - 1.dp), // we delete 1.dp to account for not-so divisible numbers
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = it.avatarUrl,
|
||||||
|
contentDescription = it.avatarUrl,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.size((itemSize / 3).coerceAtMost(40.dp))
|
||||||
|
.clip(CircleShape)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = it.username,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.width(itemSize - 1.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = it.avatarUrl,
|
||||||
|
contentDescription = it.avatarUrl,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(size = (itemSize - 1.dp).coerceAtMost(50.dp)) // we delete 1.dp to account for not-so divisible numbers
|
||||||
|
.clip(CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processHeadlineText(repositoryName: String): String {
|
fun processHeadlineText(repositoryName: String): String {
|
||||||
return "Revanced " + repositoryName.replace("revanced/revanced-", "")
|
return "ReVanced " + repositoryName.replace("revanced/revanced-", "")
|
||||||
.replace("-", " ")
|
.replace("-", " ")
|
||||||
.split(" ")
|
.split(" ").joinToString(" ") { if (it.length > 3) it else it.uppercase() }
|
||||||
.map { if (it.length > 3) it else it.uppercase() }
|
|
||||||
.joinToString(" ")
|
|
||||||
.replaceFirstChar { it.uppercase() }
|
.replaceFirstChar { it.uppercase() }
|
||||||
}
|
}
|
@ -8,12 +8,11 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -23,6 +22,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
import app.revanced.manager.ui.component.GroupHeader
|
||||||
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.component.settings.BooleanItem
|
import app.revanced.manager.ui.component.settings.BooleanItem
|
||||||
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
|
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
@ -66,12 +66,20 @@ fun DownloadsSettingsScreen(
|
|||||||
|
|
||||||
GroupHeader(stringResource(R.string.downloaded_apps))
|
GroupHeader(stringResource(R.string.downloaded_apps))
|
||||||
|
|
||||||
downloadedApps.forEach {
|
downloadedApps.forEach { app ->
|
||||||
ListItem(
|
val selected = app in viewModel.selection
|
||||||
modifier = Modifier.clickable { viewModel.toggleItem(it) },
|
|
||||||
headlineContent = { Text(it.packageName) },
|
SettingsListItem(
|
||||||
supportingContent = { Text(it.version) },
|
modifier = Modifier.clickable { viewModel.toggleItem(app) },
|
||||||
tonalElevation = if (viewModel.selection.contains(it)) 8.dp else 0.dp
|
headlineContent = app.packageName,
|
||||||
|
leadingContent = (@Composable {
|
||||||
|
Checkbox(
|
||||||
|
checked = selected,
|
||||||
|
onCheckedChange = { viewModel.toggleItem(app) }
|
||||||
|
)
|
||||||
|
}).takeIf { viewModel.selection.isNotEmpty() },
|
||||||
|
supportingContent = app.version,
|
||||||
|
tonalElevation = if (selected) 8.dp else 0.dp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import app.revanced.manager.ui.component.settings.BooleanItem
|
|||||||
import app.revanced.manager.ui.theme.Theme
|
import app.revanced.manager.ui.theme.Theme
|
||||||
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -58,12 +59,12 @@ fun GeneralSettingsScreen(
|
|||||||
GroupHeader(stringResource(R.string.appearance))
|
GroupHeader(stringResource(R.string.appearance))
|
||||||
|
|
||||||
val theme by prefs.theme.getAsState()
|
val theme by prefs.theme.getAsState()
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable { showThemePicker = true },
|
modifier = Modifier.clickable { showThemePicker = true },
|
||||||
headlineContent = { Text(stringResource(R.string.theme)) },
|
headlineContent = stringResource(R.string.theme),
|
||||||
supportingContent = { Text(stringResource(R.string.theme_description)) },
|
supportingContent = stringResource(R.string.theme_description),
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Button(
|
FilledTonalButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
showThemePicker = true
|
showThemePicker = true
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,10 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@ -14,6 +17,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.outlined.Key
|
import androidx.compose.material.icons.outlined.Key
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
@ -35,6 +39,7 @@ import app.revanced.manager.ui.component.bundle.BundleSelector
|
|||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -53,8 +58,10 @@ fun ImportExportSettingsScreen(
|
|||||||
it?.let(vm::exportKeystore)
|
it?.let(vm::exportKeystore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
|
||||||
|
|
||||||
vm.selectionAction?.let { action ->
|
vm.selectionAction?.let { action ->
|
||||||
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
|
||||||
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
|
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
vm.clearSelectionAction()
|
vm.clearSelectionAction()
|
||||||
@ -64,7 +71,7 @@ fun ImportExportSettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (vm.selectedBundle == null) {
|
if (vm.selectedBundle == null) {
|
||||||
BundleSelector(sources) {
|
BundleSelector(patchBundles) {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
vm.clearSelectionAction()
|
vm.clearSelectionAction()
|
||||||
} else {
|
} else {
|
||||||
@ -137,21 +144,120 @@ fun ImportExportSettingsScreen(
|
|||||||
headline = R.string.backup_patches_selection,
|
headline = R.string.backup_patches_selection,
|
||||||
description = R.string.backup_patches_selection_description
|
description = R.string.backup_patches_selection_description
|
||||||
)
|
)
|
||||||
|
// TODO: allow resetting selection for specific bundle or package name.
|
||||||
GroupItem(
|
GroupItem(
|
||||||
onClick = vm::resetSelection,
|
onClick = vm::resetSelection,
|
||||||
headline = R.string.clear_patches_selection,
|
headline = R.string.clear_patches_selection,
|
||||||
description = R.string.clear_patches_selection_description
|
description = R.string.clear_patches_selection_description
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var showPackageSelector by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
var showBundleSelector by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPackageSelector)
|
||||||
|
PackageSelector(packages = packagesWithOptions) { selected ->
|
||||||
|
selected?.let(vm::clearOptionsForPackage)
|
||||||
|
|
||||||
|
showPackageSelector = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBundleSelector)
|
||||||
|
BundleSelector(bundles = patchBundles) { bundle ->
|
||||||
|
bundle?.let(vm::clearOptionsForBundle)
|
||||||
|
|
||||||
|
showBundleSelector = false
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupHeader(stringResource(R.string.patch_options))
|
||||||
|
// TODO: patch options import/export.
|
||||||
|
GroupItem(
|
||||||
|
onClick = { showPackageSelector = true },
|
||||||
|
headline = R.string.patch_options_clear_package,
|
||||||
|
description = R.string.patch_options_clear_package_description
|
||||||
|
)
|
||||||
|
if (patchBundles.size > 1)
|
||||||
|
GroupItem(
|
||||||
|
onClick = { showBundleSelector = true },
|
||||||
|
headline = R.string.patch_options_clear_bundle,
|
||||||
|
description = R.string.patch_options_clear_bundle_description,
|
||||||
|
)
|
||||||
|
GroupItem(
|
||||||
|
onClick = vm::resetOptions,
|
||||||
|
headline = R.string.patch_options_clear_all,
|
||||||
|
description = R.string.patch_options_clear_all_description,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun PackageSelector(packages: Set<String>, onFinish: (String?) -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val noPackages = packages.isEmpty()
|
||||||
|
|
||||||
|
LaunchedEffect(noPackages) {
|
||||||
|
if (noPackages) {
|
||||||
|
context.toast("No packages available.")
|
||||||
|
onFinish(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noPackages) return
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { onFinish(null) }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Select package",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
packages.forEach {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
onFinish(it)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes description: Int) =
|
private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes description: Int) =
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable { onClick() },
|
modifier = Modifier.clickable { onClick() },
|
||||||
headlineContent = { Text(stringResource(headline)) },
|
headlineContent = stringResource(headline),
|
||||||
supportingContent = { Text(stringResource(description)) }
|
supportingContent = stringResource(description)
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
package app.revanced.manager.ui.screen.settings.update
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
|
import app.revanced.manager.ui.component.settings.Changelog
|
||||||
|
import app.revanced.manager.ui.viewmodel.ChangelogsViewModel
|
||||||
|
import app.revanced.manager.util.formatNumber
|
||||||
|
import app.revanced.manager.util.relativeTime
|
||||||
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ChangelogsScreen(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
vm: ChangelogsViewModel = getViewModel()
|
||||||
|
) {
|
||||||
|
val changelogs = vm.changelogs
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
AppTopBar(
|
||||||
|
title = stringResource(R.string.changelog),
|
||||||
|
onBackClick = onBackClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = if (changelogs.isNullOrEmpty()) Arrangement.Center else Arrangement.Top
|
||||||
|
) {
|
||||||
|
if (changelogs == null) {
|
||||||
|
item {
|
||||||
|
LoadingIndicator()
|
||||||
|
}
|
||||||
|
} else if (changelogs.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.no_changelogs_found),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val lastChangelog = changelogs.last()
|
||||||
|
items(
|
||||||
|
changelogs,
|
||||||
|
key = { it.version }
|
||||||
|
) { changelog ->
|
||||||
|
ChangelogItem(changelog, lastChangelog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChangelogItem(
|
||||||
|
changelog: ChangelogsViewModel.Changelog,
|
||||||
|
lastChangelog: ChangelogsViewModel.Changelog
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Changelog(
|
||||||
|
markdown = changelog.body.replace("`", ""),
|
||||||
|
version = changelog.version,
|
||||||
|
downloadCount = changelog.downloadCount.formatNumber(),
|
||||||
|
publishDate = changelog.publishDate.relativeTime(LocalContext.current)
|
||||||
|
)
|
||||||
|
if (changelog != lastChangelog) {
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(top = 32.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,100 +0,0 @@
|
|||||||
package app.revanced.manager.ui.screen.settings.update
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Campaign
|
|
||||||
import androidx.compose.material.icons.outlined.FileDownload
|
|
||||||
import androidx.compose.material.icons.outlined.Sell
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
|
||||||
import app.revanced.manager.ui.component.Markdown
|
|
||||||
import app.revanced.manager.ui.viewmodel.ManagerUpdateChangelogViewModel
|
|
||||||
import org.koin.androidx.compose.getViewModel
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun ManagerUpdateChangelog(
|
|
||||||
onBackClick: () -> Unit,
|
|
||||||
vm: ManagerUpdateChangelogViewModel = getViewModel()
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
AppTopBar(
|
|
||||||
title = stringResource(R.string.changelog),
|
|
||||||
onBackClick = onBackClick
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Campaign,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(32.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
vm.changelog.version.removePrefix("v"),
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Sell,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
vm.changelog.version,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Markdown(
|
|
||||||
vm.changelogHtml,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
package app.revanced.manager.ui.screen.settings.update
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.Stable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
|
||||||
import app.revanced.manager.ui.viewmodel.UpdateProgressViewModel
|
|
||||||
import org.koin.androidx.compose.getViewModel
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
@Stable
|
|
||||||
fun UpdateProgressScreen(
|
|
||||||
onBackClick: () -> Unit,
|
|
||||||
vm: UpdateProgressViewModel = getViewModel()
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
AppTopBar(
|
|
||||||
title = stringResource(R.string.updates),
|
|
||||||
onBackClick = onBackClick
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(vertical = 16.dp, horizontal = 24.dp)
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = if (vm.isInstalling) stringResource(R.string.installing_manager_update) else stringResource(
|
|
||||||
R.string.downloading_manager_update
|
|
||||||
), style = MaterialTheme.typography.headlineMedium
|
|
||||||
)
|
|
||||||
LinearProgressIndicator(
|
|
||||||
progress = vm.downloadProgress,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 16.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = if (!vm.isInstalling) "${vm.downloadedSize.div(1000000)} MB / ${
|
|
||||||
vm.totalSize.div(
|
|
||||||
1000000
|
|
||||||
)
|
|
||||||
} MB (${vm.downloadProgress.times(100).toInt()}%)" else stringResource(R.string.installing_message),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "This update adds many functionality and fixes many issues in Manager. New experiment toggles are also added, they can be found in Settings > Advanced. Please submit some feedback in Settings > About > Submit issues or feedback. Thank you, everyone!",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier.padding(vertical = 32.dp),
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.Bottom,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
TextButton(
|
|
||||||
onClick = onBackClick,
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(R.string.cancel))
|
|
||||||
}
|
|
||||||
Button(onClick = vm::installUpdate, enabled = vm.finished) {
|
|
||||||
Text(text = stringResource(R.string.update))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,239 @@
|
|||||||
|
package app.revanced.manager.ui.screen.settings.update
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Update
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.revanced.manager.BuildConfig
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.settings.Changelog
|
||||||
|
import app.revanced.manager.ui.viewmodel.UpdateViewModel
|
||||||
|
import app.revanced.manager.ui.viewmodel.UpdateViewModel.Changelog
|
||||||
|
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
|
||||||
|
import app.revanced.manager.util.formatNumber
|
||||||
|
import app.revanced.manager.util.relativeTime
|
||||||
|
import com.gigamole.composefadingedges.content.FadingEdgesContentType
|
||||||
|
import com.gigamole.composefadingedges.content.scrollconfig.FadingEdgesScrollConfig
|
||||||
|
import com.gigamole.composefadingedges.fill.FadingEdgesFillType
|
||||||
|
import com.gigamole.composefadingedges.verticalFadingEdges
|
||||||
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
@Stable
|
||||||
|
fun UpdateScreen(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
vm: UpdateViewModel = getViewModel()
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
AppTopBar(
|
||||||
|
title = stringResource(R.string.updates),
|
||||||
|
onBackClick = onBackClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
AnimatedVisibility(visible = vm.showInternetCheckDialog) {
|
||||||
|
MeteredDownloadConfirmationDialog(
|
||||||
|
onDismiss = { vm.showInternetCheckDialog = false },
|
||||||
|
onDownloadAnyways = { vm.downloadUpdate(true) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(vertical = 16.dp, horizontal = 24.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||||
|
) {
|
||||||
|
Header(
|
||||||
|
vm.state,
|
||||||
|
vm.changelog,
|
||||||
|
DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize)
|
||||||
|
)
|
||||||
|
vm.changelog?.let { changelog ->
|
||||||
|
Divider()
|
||||||
|
Changelog(changelog)
|
||||||
|
} ?: Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Buttons(vm.state, vm::downloadUpdate, vm::installUpdate, onBackClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MeteredDownloadConfirmationDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onDownloadAnyways: () -> Unit
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onDismiss) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onDismiss()
|
||||||
|
onDownloadAnyways()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.download))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text(stringResource(R.string.download_update_confirmation)) },
|
||||||
|
icon = { Icon(Icons.Outlined.Update, null) },
|
||||||
|
text = { Text(stringResource(R.string.download_confirmation_metered)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Header(state: State, changelog: Changelog?, downloadData: DownloadData) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(state.title),
|
||||||
|
style = MaterialTheme.typography.headlineMedium
|
||||||
|
)
|
||||||
|
if (state == State.CAN_DOWNLOAD) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.current_version,
|
||||||
|
BuildConfig.VERSION_NAME
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
changelog?.let { changelog ->
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.new_version,
|
||||||
|
changelog.version.replace("v", "")
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (state == State.DOWNLOADING) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = downloadData.downloadProgress,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
"${downloadData.downloadedSize.div(1000000)} MB / ${
|
||||||
|
downloadData.totalSize.div(
|
||||||
|
1000000
|
||||||
|
)
|
||||||
|
} MB (${
|
||||||
|
downloadData.downloadProgress.times(
|
||||||
|
100
|
||||||
|
).toInt()
|
||||||
|
}%)",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColumnScope.Changelog(changelog: Changelog) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.verticalFadingEdges(
|
||||||
|
fillType = FadingEdgesFillType.FadeColor(
|
||||||
|
color = MaterialTheme.colorScheme.background,
|
||||||
|
fillStops = Triple(0F, 0.55F, 1F),
|
||||||
|
secondStopAlpha = 1F
|
||||||
|
),
|
||||||
|
contentType = FadingEdgesContentType.Dynamic.Scroll(
|
||||||
|
state = scrollState,
|
||||||
|
scrollConfig = FadingEdgesScrollConfig.Dynamic(
|
||||||
|
animationSpec = spring(),
|
||||||
|
isLerpByDifferenceForPartialContent = true,
|
||||||
|
scrollFactor = 1.25F
|
||||||
|
)
|
||||||
|
),
|
||||||
|
length = 350.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Changelog(
|
||||||
|
markdown = changelog.body.replace("`", ""),
|
||||||
|
version = changelog.version,
|
||||||
|
downloadCount = changelog.downloadCount.formatNumber(),
|
||||||
|
publishDate = changelog.publishDate.relativeTime(LocalContext.current)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Buttons(
|
||||||
|
state: State,
|
||||||
|
onDownloadClick: () -> Unit,
|
||||||
|
onInstallClick: () -> Unit,
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
if (state.showCancel) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onBackClick,
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
if (state == State.CAN_DOWNLOAD) {
|
||||||
|
Button(onClick = onDownloadClick) {
|
||||||
|
Text(text = stringResource(R.string.update))
|
||||||
|
}
|
||||||
|
} else if (state == State.CAN_INSTALL) {
|
||||||
|
Button(
|
||||||
|
onClick = onInstallClick
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.install_app))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DownloadData(
|
||||||
|
val downloadProgress: Float,
|
||||||
|
val downloadedSize: Long,
|
||||||
|
val totalSize: Long
|
||||||
|
)
|
@ -1,33 +1,24 @@
|
|||||||
package app.revanced.manager.ui.screen.settings.update
|
package app.revanced.manager.ui.screen.settings.update
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
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.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Update
|
import androidx.compose.material.icons.filled.Update
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
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
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.NotificationCard
|
||||||
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -69,59 +60,22 @@ fun UpdatesSettingsScreen(
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
UpdateNotification(
|
NotificationCard(
|
||||||
onClick = onUpdateClick
|
text = stringResource(R.string.update_notification),
|
||||||
|
icon = Icons.Default.Update,
|
||||||
|
primaryAction = onUpdateClick
|
||||||
)
|
)
|
||||||
|
|
||||||
listItems.forEach { (title, description, onClick) ->
|
listItems.forEach { (title, description, onClick) ->
|
||||||
ListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp)
|
.padding(horizontal = 8.dp)
|
||||||
.clickable { onClick() },
|
.clickable { onClick() },
|
||||||
headlineContent = {
|
headlineContent = title,
|
||||||
Text(
|
supportingContent = description
|
||||||
title,
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
},
|
|
||||||
supportingContent = {
|
|
||||||
Text(
|
|
||||||
description,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun UpdateNotification(
|
|
||||||
onClick: () -> Unit
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
.clip(RoundedCornerShape(24.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.secondaryContainer)
|
|
||||||
.clickable { onClick() },
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
Icon(imageVector = Icons.Default.Update, contentDescription = null)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.update_notification),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -12,17 +12,14 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class AdvancedSettingsViewModel(
|
class AdvancedSettingsViewModel(
|
||||||
prefs: PreferencesManager,
|
val prefs: PreferencesManager,
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val patchBundleRepository: PatchBundleRepository
|
private val patchBundleRepository: PatchBundleRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val apiUrl = prefs.api
|
|
||||||
val allowExperimental = prefs.allowExperimental
|
|
||||||
|
|
||||||
fun setApiUrl(value: String) = viewModelScope.launch(Dispatchers.Default) {
|
fun setApiUrl(value: String) = viewModelScope.launch(Dispatchers.Default) {
|
||||||
if (value == apiUrl.get()) return@launch
|
if (value == prefs.api.get()) return@launch
|
||||||
|
|
||||||
apiUrl.update(value)
|
prefs.api.update(value)
|
||||||
patchBundleRepository.reloadApiBundles()
|
patchBundleRepository.reloadApiBundles()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
|
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
|
||||||
|
import app.revanced.manager.network.utils.getOrNull
|
||||||
|
import app.revanced.manager.util.APK_MIMETYPE
|
||||||
|
import app.revanced.manager.util.uiSafe
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class ChangelogsViewModel(
|
||||||
|
private val api: ReVancedAPI,
|
||||||
|
private val app: Application,
|
||||||
|
) : ViewModel() {
|
||||||
|
var changelogs: List<Changelog>? by mutableStateOf(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") {
|
||||||
|
changelogs = api.getReleases("revanced-manager").getOrNull().orEmpty().map { release ->
|
||||||
|
Changelog(
|
||||||
|
release.metadata.tag,
|
||||||
|
release.findAssetByType(APK_MIMETYPE).downloadCount,
|
||||||
|
release.metadata.publishedAt,
|
||||||
|
release.metadata.body
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Changelog(
|
||||||
|
val version: String,
|
||||||
|
val downloadCount: Int,
|
||||||
|
val publishDate: String,
|
||||||
|
val body: String,
|
||||||
|
)
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.network.api.ReVancedAPI
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
@ -11,13 +14,14 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class ContributorViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() {
|
class ContributorViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() {
|
||||||
val repositories = mutableStateListOf<ReVancedGitRepository>()
|
var repositories: List<ReVancedGitRepository>? by mutableStateOf(null)
|
||||||
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) { reVancedAPI.getContributors().getOrNull() }?.let(
|
repositories = withContext(Dispatchers.IO) {
|
||||||
repositories::addAll
|
reVancedAPI.getContributors().getOrNull()
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,6 +16,7 @@ import app.revanced.manager.domain.repository.PatchSelectionRepository
|
|||||||
import app.revanced.manager.domain.repository.SerializedSelection
|
import app.revanced.manager.domain.repository.SerializedSelection
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||||
import app.revanced.manager.util.JSON_MIMETYPE
|
import app.revanced.manager.util.JSON_MIMETYPE
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
@ -38,10 +39,11 @@ class ImportExportViewModel(
|
|||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val keystoreManager: KeystoreManager,
|
private val keystoreManager: KeystoreManager,
|
||||||
private val selectionRepository: PatchSelectionRepository,
|
private val selectionRepository: PatchSelectionRepository,
|
||||||
|
private val optionsRepository: PatchOptionsRepository,
|
||||||
patchBundleRepository: PatchBundleRepository
|
patchBundleRepository: PatchBundleRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val contentResolver = app.contentResolver
|
private val contentResolver = app.contentResolver
|
||||||
val sources = patchBundleRepository.sources
|
val patchBundles = patchBundleRepository.sources
|
||||||
var selectedBundle by mutableStateOf<PatchBundleSource?>(null)
|
var selectedBundle by mutableStateOf<PatchBundleSource?>(null)
|
||||||
private set
|
private set
|
||||||
var selectionAction by mutableStateOf<SelectionAction?>(null)
|
var selectionAction by mutableStateOf<SelectionAction?>(null)
|
||||||
@ -49,6 +51,20 @@ class ImportExportViewModel(
|
|||||||
private var keystoreImportPath by mutableStateOf<Path?>(null)
|
private var keystoreImportPath by mutableStateOf<Path?>(null)
|
||||||
val showCredentialsDialog by derivedStateOf { keystoreImportPath != null }
|
val showCredentialsDialog by derivedStateOf { keystoreImportPath != null }
|
||||||
|
|
||||||
|
val packagesWithOptions = optionsRepository.getPackagesWithSavedOptions()
|
||||||
|
|
||||||
|
fun clearOptionsForPackage(packageName: String) = viewModelScope.launch {
|
||||||
|
optionsRepository.clearOptionsForPackage(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearOptionsForBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
|
||||||
|
optionsRepository.clearOptionsForPatchBundle(patchBundle.uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetOptions() = viewModelScope.launch {
|
||||||
|
optionsRepository.reset()
|
||||||
|
}
|
||||||
|
|
||||||
fun startKeystoreImport(content: Uri) = viewModelScope.launch {
|
fun startKeystoreImport(content: Uri) = viewModelScope.launch {
|
||||||
val path = withContext(Dispatchers.IO) {
|
val path = withContext(Dispatchers.IO) {
|
||||||
File.createTempFile("signing", "ks", app.cacheDir).toPath().also {
|
File.createTempFile("signing", "ks", app.cacheDir).toPath().also {
|
||||||
|
@ -11,6 +11,7 @@ import android.util.Log
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
@ -30,7 +31,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
class AppInfoViewModel(
|
class InstalledAppInfoViewModel(
|
||||||
val installedApp: InstalledApp
|
val installedApp: InstalledApp
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
@ -83,8 +84,10 @@ class AppInfoViewModel(
|
|||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||||
val extraStatus = intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
|
val extraStatus =
|
||||||
val extraStatusMessage = intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
|
||||||
|
val extraStatusMessage =
|
||||||
|
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||||
|
|
||||||
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
|
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@ -113,9 +116,11 @@ class AppInfoViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.registerReceiver(
|
ContextCompat.registerReceiver(
|
||||||
|
app,
|
||||||
uninstallBroadcastReceiver,
|
uninstallBroadcastReceiver,
|
||||||
IntentFilter(UninstallService.APP_UNINSTALL_ACTION)
|
IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import androidx.compose.runtime.derivedStateOf
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@ -23,7 +24,6 @@ import app.revanced.manager.data.platform.Filesystem
|
|||||||
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.domain.installer.RootInstaller
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
import app.revanced.manager.domain.manager.KeystoreManager
|
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
import app.revanced.manager.patcher.worker.PatcherProgressManager
|
import app.revanced.manager.patcher.worker.PatcherProgressManager
|
||||||
@ -56,7 +56,6 @@ import java.util.logging.LogRecord
|
|||||||
class InstallerViewModel(
|
class InstallerViewModel(
|
||||||
private val input: Destination.Installer
|
private val input: Destination.Installer
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
private val keystoreManager: KeystoreManager by inject()
|
|
||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
private val fs: Filesystem by inject()
|
private val fs: Filesystem by inject()
|
||||||
private val pm: PM by inject()
|
private val pm: PM by inject()
|
||||||
@ -71,8 +70,6 @@ class InstallerViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val outputFile = tempDir.resolve("output.apk")
|
private val outputFile = tempDir.resolve("output.apk")
|
||||||
private val signedFile = tempDir.resolve("signed.apk")
|
|
||||||
private var hasSigned = false
|
|
||||||
private var inputFile: File? = null
|
private var inputFile: File? = null
|
||||||
|
|
||||||
private var installedApp: InstalledApp? = null
|
private var installedApp: InstalledApp? = null
|
||||||
@ -97,11 +94,13 @@ class InstallerViewModel(
|
|||||||
|
|
||||||
val (selectedApp, patches, options) = input
|
val (selectedApp, patches, options) = input
|
||||||
|
|
||||||
_progress = MutableStateFlow(PatcherProgressManager.generateSteps(
|
_progress = MutableStateFlow(
|
||||||
app,
|
PatcherProgressManager.generateSteps(
|
||||||
patches.flatMap { (_, selected) -> selected },
|
app,
|
||||||
selectedApp
|
patches.flatMap { (_, selected) -> selected },
|
||||||
).toImmutableList())
|
selectedApp
|
||||||
|
).toImmutableList()
|
||||||
|
)
|
||||||
|
|
||||||
patcherWorkerId =
|
patcherWorkerId =
|
||||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||||
@ -140,7 +139,7 @@ class InstallerViewModel(
|
|||||||
installedPackageName =
|
installedPackageName =
|
||||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
installedAppRepository.add(
|
installedAppRepository.addOrUpdate(
|
||||||
installedPackageName!!,
|
installedPackageName!!,
|
||||||
packageName,
|
packageName,
|
||||||
input.selectedApp.version,
|
input.selectedApp.version,
|
||||||
@ -160,10 +159,10 @@ class InstallerViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
app.registerReceiver(installBroadcastReceiver, IntentFilter().apply {
|
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
|
||||||
addAction(InstallService.APP_INSTALL_ACTION)
|
addAction(InstallService.APP_INSTALL_ACTION)
|
||||||
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
||||||
})
|
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportLogs(context: Context) {
|
fun exportLogs(context: Context) {
|
||||||
@ -186,62 +185,47 @@ class InstallerViewModel(
|
|||||||
is SelectedApp.Local -> {
|
is SelectedApp.Local -> {
|
||||||
if (selectedApp.shouldDelete) selectedApp.file.delete()
|
if (selectedApp.shouldDelete) selectedApp.file.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is SelectedApp.Installed -> {
|
||||||
|
try {
|
||||||
|
installedApp?.let {
|
||||||
|
if (it.installType == InstallType.ROOT) {
|
||||||
|
rootInstaller.mount(packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(tag, "Failed to mount", e)
|
||||||
|
app.toast(app.getString(R.string.failed_to_mount, e.simpleMessage()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
tempDir.deleteRecursively()
|
tempDir.deleteRecursively()
|
||||||
|
|
||||||
try {
|
|
||||||
if (input.selectedApp is SelectedApp.Installed) {
|
|
||||||
installedApp?.let {
|
|
||||||
if (it.installType == InstallType.ROOT) {
|
|
||||||
rootInstaller.mount(packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(tag, "Failed to mount", e)
|
|
||||||
app.toast(app.getString(R.string.failed_to_mount, e.simpleMessage()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun signApk(): Boolean {
|
|
||||||
if (!hasSigned) {
|
|
||||||
try {
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
keystoreManager.sign(outputFile, signedFile)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(tag, "Got exception while signing", e)
|
|
||||||
app.toast(app.getString(R.string.sign_fail, e::class.simpleName))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun export(uri: Uri?) = viewModelScope.launch {
|
fun export(uri: Uri?) = viewModelScope.launch {
|
||||||
uri?.let {
|
uri?.let {
|
||||||
if (signApk()) {
|
withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
app.contentResolver.openOutputStream(it)
|
||||||
app.contentResolver.openOutputStream(it)
|
.use { stream -> Files.copy(outputFile.toPath(), stream) }
|
||||||
.use { stream -> Files.copy(signedFile.toPath(), stream) }
|
|
||||||
}
|
|
||||||
app.toast(app.getString(R.string.export_app_success))
|
|
||||||
}
|
}
|
||||||
|
app.toast(app.getString(R.string.save_apk_success))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun install(installType: InstallType) = viewModelScope.launch {
|
fun install(installType: InstallType) = viewModelScope.launch {
|
||||||
isInstalling = true
|
isInstalling = true
|
||||||
try {
|
try {
|
||||||
if (!signApk()) return@launch
|
|
||||||
|
|
||||||
when (installType) {
|
when (installType) {
|
||||||
InstallType.DEFAULT -> { pm.installApp(listOf(signedFile)) }
|
InstallType.DEFAULT -> {
|
||||||
|
pm.installApp(listOf(outputFile))
|
||||||
|
}
|
||||||
|
|
||||||
InstallType.ROOT -> { installAsRoot() }
|
InstallType.ROOT -> {
|
||||||
|
installAsRoot()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
@ -254,7 +238,7 @@ class InstallerViewModel(
|
|||||||
private suspend fun installAsRoot() {
|
private suspend fun installAsRoot() {
|
||||||
try {
|
try {
|
||||||
val label = with(pm) {
|
val label = with(pm) {
|
||||||
getPackageInfo(signedFile)?.label()
|
getPackageInfo(outputFile)?.label()
|
||||||
?: throw Exception("Failed to load application info")
|
?: throw Exception("Failed to load application info")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,7 +254,7 @@ class InstallerViewModel(
|
|||||||
|
|
||||||
installedApp?.let { installedAppRepository.delete(it) }
|
installedApp?.let { installedAppRepository.delete(it) }
|
||||||
|
|
||||||
installedAppRepository.add(
|
installedAppRepository.addOrUpdate(
|
||||||
packageName,
|
packageName,
|
||||||
packageName,
|
packageName,
|
||||||
input.selectedApp.version,
|
input.selectedApp.version,
|
||||||
@ -286,7 +270,8 @@ class InstallerViewModel(
|
|||||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||||
try {
|
try {
|
||||||
rootInstaller.uninstall(packageName)
|
rootInstaller.uninstall(packageName)
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,77 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.data.platform.NetworkInfo
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull
|
||||||
|
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.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||||
|
import app.revanced.manager.domain.repository.SerializedSelection
|
||||||
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
|
import app.revanced.manager.network.utils.getOrThrow
|
||||||
|
import app.revanced.manager.ui.theme.Theme
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import app.revanced.manager.util.toast
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
private val patchBundleRepository: PatchBundleRepository,
|
private val patchBundleRepository: PatchBundleRepository,
|
||||||
|
private val patchSelectionRepository: PatchSelectionRepository,
|
||||||
|
private val keystoreManager: KeystoreManager,
|
||||||
|
private val reVancedAPI: ReVancedAPI,
|
||||||
|
private val app: Application,
|
||||||
|
private val networkInfo: NetworkInfo,
|
||||||
val prefs: PreferencesManager
|
val prefs: PreferencesManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
var updatedManagerVersion: String? by mutableStateOf(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch { checkForManagerUpdates() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissUpdateDialog() {
|
||||||
|
updatedManagerVersion = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun checkForManagerUpdates() {
|
||||||
|
if (!prefs.managerAutoUpdates.get() || !networkInfo.isConnected()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
reVancedAPI.getLatestRelease("revanced-manager").getOrThrow().let { release ->
|
||||||
|
updatedManagerVersion = release.metadata.tag.takeIf { it != Build.VERSION.RELEASE }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
app.toast(app.getString(R.string.failed_to_check_updates))
|
||||||
|
Log.e(tag, "Failed to check for updates", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch {
|
fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch {
|
||||||
prefs.showAutoUpdatesDialog.update(false)
|
prefs.firstLaunch.update(false)
|
||||||
|
|
||||||
prefs.managerAutoUpdates.update(manager)
|
prefs.managerAutoUpdates.update(manager)
|
||||||
|
|
||||||
|
if (manager) checkForManagerUpdates()
|
||||||
|
|
||||||
if (patches) {
|
if (patches) {
|
||||||
with(patchBundleRepository) {
|
with(patchBundleRepository) {
|
||||||
sources
|
sources
|
||||||
@ -30,4 +84,97 @@ class MainViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
fun importLegacySettings(componentActivity: ComponentActivity) {
|
||||||
|
if (!prefs.firstLaunch.getBlocking()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
val launcher = componentActivity.registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result: ActivityResult ->
|
||||||
|
if (result.resultCode == ComponentActivity.RESULT_OK) {
|
||||||
|
result.data?.getStringExtra("data")?.let {
|
||||||
|
applyLegacySettings(it)
|
||||||
|
} ?: app.toast(app.getString(R.string.legacy_import_failed))
|
||||||
|
} else {
|
||||||
|
app.toast(app.getString(R.string.legacy_import_failed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent().apply {
|
||||||
|
setClassName(
|
||||||
|
"app.revanced.manager.flutter",
|
||||||
|
"app.revanced.manager.flutter.ExportSettingsActivity"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
launcher.launch(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e !is ActivityNotFoundException) {
|
||||||
|
app.toast(app.getString(R.string.legacy_import_failed))
|
||||||
|
Log.e(tag, "Failed to launch legacy import activity: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyLegacySettings(data: String) = viewModelScope.launch {
|
||||||
|
val json = Json { ignoreUnknownKeys = true }
|
||||||
|
val settings = json.decodeFromString<LegacySettings>(data)
|
||||||
|
|
||||||
|
settings.themeMode?.let { theme ->
|
||||||
|
val themeMap = mapOf(
|
||||||
|
0 to Theme.SYSTEM,
|
||||||
|
1 to Theme.LIGHT,
|
||||||
|
2 to Theme.DARK
|
||||||
|
)
|
||||||
|
prefs.theme.update(themeMap[theme] ?: Theme.SYSTEM)
|
||||||
|
}
|
||||||
|
settings.useDynamicTheme?.let { dynamicColor ->
|
||||||
|
prefs.dynamicColor.update(dynamicColor)
|
||||||
|
}
|
||||||
|
settings.apiUrl?.let { api ->
|
||||||
|
prefs.api.update(api.removeSuffix("/"))
|
||||||
|
}
|
||||||
|
settings.experimentalPatchesEnabled?.let { allowExperimental ->
|
||||||
|
prefs.allowExperimental.update(allowExperimental)
|
||||||
|
}
|
||||||
|
settings.patchesAutoUpdate?.let { autoUpdate ->
|
||||||
|
with(patchBundleRepository) {
|
||||||
|
sources
|
||||||
|
.first()
|
||||||
|
.find { it.uid == 0 }
|
||||||
|
?.asRemoteOrNull
|
||||||
|
?.setAutoUpdate(autoUpdate)
|
||||||
|
|
||||||
|
updateCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings.patchesChangeEnabled?.let { disableSelectionWarning ->
|
||||||
|
prefs.disableSelectionWarning.update(disableSelectionWarning)
|
||||||
|
}
|
||||||
|
settings.keystore?.let { keystore ->
|
||||||
|
val keystoreBytes = Base64.decode(keystore, Base64.DEFAULT)
|
||||||
|
keystoreManager.import(
|
||||||
|
"ReVanced",
|
||||||
|
settings.keystorePassword,
|
||||||
|
keystoreBytes.inputStream()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
settings.patches?.let { selection ->
|
||||||
|
patchSelectionRepository.import(0, selection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class LegacySettings(
|
||||||
|
val keystorePassword: String,
|
||||||
|
val themeMode: Int? = null,
|
||||||
|
val useDynamicTheme: Boolean? = null,
|
||||||
|
val apiUrl: String? = null,
|
||||||
|
val experimentalPatchesEnabled: Boolean? = null,
|
||||||
|
val patchesAutoUpdate: Boolean? = null,
|
||||||
|
val patchesChangeEnabled: Boolean? = null,
|
||||||
|
val keystore: String? = null,
|
||||||
|
val patches: SerializedSelection? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.network.api.ReVancedAPI
|
|
||||||
import app.revanced.manager.network.utils.getOrThrow
|
|
||||||
import app.revanced.manager.util.uiSafe
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
|
||||||
import org.intellij.markdown.html.HtmlGenerator
|
|
||||||
import org.intellij.markdown.parser.MarkdownParser
|
|
||||||
|
|
||||||
class ManagerUpdateChangelogViewModel(
|
|
||||||
private val api: ReVancedAPI,
|
|
||||||
private val app: Application,
|
|
||||||
) : ViewModel() {
|
|
||||||
private val markdownFlavour = GFMFlavourDescriptor()
|
|
||||||
private val markdownParser = MarkdownParser(flavour = markdownFlavour)
|
|
||||||
|
|
||||||
var changelog by mutableStateOf(
|
|
||||||
Changelog(
|
|
||||||
"...",
|
|
||||||
app.getString(R.string.changelog_loading),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
private set
|
|
||||||
val changelogHtml by derivedStateOf {
|
|
||||||
val markdown = changelog.body
|
|
||||||
val parsedTree = markdownParser.buildMarkdownTreeFromString(markdown)
|
|
||||||
HtmlGenerator(markdown, parsedTree, markdownFlavour).generateHtml()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") {
|
|
||||||
changelog = api.getRelease("revanced-manager").getOrThrow().let {
|
|
||||||
Changelog(it.metadata.tag, it.metadata.body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Changelog(
|
|
||||||
val version: String,
|
|
||||||
val body: String,
|
|
||||||
)
|
|
||||||
}
|
|
@ -2,8 +2,8 @@ package app.revanced.manager.ui.viewmodel
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
@ -17,69 +17,45 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
|||||||
import androidx.lifecycle.viewmodel.compose.saveable
|
import androidx.lifecycle.viewmodel.compose.saveable
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.ui.destination.Destination
|
import app.revanced.manager.ui.model.BundleInfo
|
||||||
|
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
||||||
|
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
||||||
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchesSelection
|
import app.revanced.manager.util.PatchesSelection
|
||||||
import app.revanced.manager.util.flatMapLatestAndCombine
|
import app.revanced.manager.util.saver.Nullable
|
||||||
|
import app.revanced.manager.util.saver.nullableSaver
|
||||||
|
import app.revanced.manager.util.saver.persistentMapSaver
|
||||||
|
import app.revanced.manager.util.saver.persistentSetSaver
|
||||||
import app.revanced.manager.util.saver.snapshotStateMapSaver
|
import app.revanced.manager.util.saver.snapshotStateMapSaver
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
import kotlinx.collections.immutable.*
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@OptIn(SavedStateHandleSaveableApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class)
|
||||||
class PatchesSelectorViewModel(
|
class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||||
val input: Destination.PatchesSelector
|
|
||||||
) : ViewModel(), KoinComponent {
|
|
||||||
private val app: Application = get()
|
private val app: Application = get()
|
||||||
private val selectionRepository: PatchSelectionRepository = get()
|
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
private val prefs: PreferencesManager = get()
|
private val prefs: PreferencesManager = get()
|
||||||
|
|
||||||
private val packageName = input.selectedApp.packageName
|
private val packageName = input.app.packageName
|
||||||
|
val appVersion = input.app.version
|
||||||
|
|
||||||
var pendingSelectionAction by mutableStateOf<(() -> Unit)?>(null)
|
var pendingSelectionAction by mutableStateOf<(() -> Unit)?>(null)
|
||||||
|
|
||||||
var selectionWarningEnabled by mutableStateOf(true)
|
var selectionWarningEnabled by mutableStateOf(true)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val allowExperimental = get<PreferencesManager>().allowExperimental
|
val allowExperimental = get<PreferencesManager>().allowExperimental.getBlocking()
|
||||||
val bundlesFlow = get<PatchBundleRepository>().sources.flatMapLatestAndCombine(
|
val bundlesFlow =
|
||||||
combiner = { it.filterNotNull() }
|
get<PatchBundleRepository>().bundleInfoFlow(packageName, input.app.version)
|
||||||
) { source ->
|
|
||||||
// Regenerate bundle information whenever this source updates.
|
|
||||||
source.state.map { state ->
|
|
||||||
val bundle = state.patchBundleOrNull() ?: return@map null
|
|
||||||
|
|
||||||
val supported = mutableListOf<PatchInfo>()
|
|
||||||
val unsupported = mutableListOf<PatchInfo>()
|
|
||||||
val universal = mutableListOf<PatchInfo>()
|
|
||||||
|
|
||||||
bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
|
|
||||||
val targetList = when {
|
|
||||||
it.compatiblePackages == null -> universal
|
|
||||||
it.supportsVersion(
|
|
||||||
input.selectedApp.packageName,
|
|
||||||
input.selectedApp.version
|
|
||||||
) -> supported
|
|
||||||
|
|
||||||
else -> unsupported
|
|
||||||
}
|
|
||||||
|
|
||||||
targetList.add(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
BundleInfo(source.name, source.uid, bundle.patches, supported, unsupported, universal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@ -88,63 +64,28 @@ class PatchesSelectorViewModel(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val experimental = allowExperimental.get()
|
fun BundleInfo.hasDefaultPatches() = patchSequence(allowExperimental).any { it.include }
|
||||||
fun BundleInfo.hasDefaultPatches(): Boolean {
|
|
||||||
return if (experimental) {
|
|
||||||
all.asSequence()
|
|
||||||
} else {
|
|
||||||
sequence {
|
|
||||||
yieldAll(supported)
|
|
||||||
yieldAll(universal)
|
|
||||||
}
|
|
||||||
}.any { it.include }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't show the warning if there are no default patches.
|
// Don't show the warning if there are no default patches.
|
||||||
selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches)
|
selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var baseSelectionMode by mutableStateOf(BaseSelectionMode.DEFAULT)
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val previousPatchesSelection: SnapshotStateMap<Int, Set<String>> = mutableStateMapOf()
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch(Dispatchers.Default) { loadPreviousSelection() }
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasPreviousSelection by derivedStateOf {
|
|
||||||
previousPatchesSelection.filterValues(Set<String>::isNotEmpty).isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var hasModifiedSelection = false
|
private var hasModifiedSelection = false
|
||||||
|
private var customPatchesSelection: PersistentPatchesSelection? by savedStateHandle.saveable(
|
||||||
|
key = "selection",
|
||||||
|
stateSaver = patchesSaver,
|
||||||
|
) {
|
||||||
|
mutableStateOf(input.currentSelection?.toPersistentPatchesSelection())
|
||||||
|
}
|
||||||
|
|
||||||
private val explicitPatchesSelection: SnapshotExplicitPatchesSelection by savedStateHandle.saveable(
|
private val patchOptions: PersistentOptions by savedStateHandle.saveable(
|
||||||
saver = explicitPatchesSelectionSaver,
|
|
||||||
init = ::mutableStateMapOf
|
|
||||||
)
|
|
||||||
|
|
||||||
private val patchOptions: SnapshotOptions by savedStateHandle.saveable(
|
|
||||||
saver = optionsSaver,
|
saver = optionsSaver,
|
||||||
init = ::mutableStateMapOf
|
) {
|
||||||
)
|
// Convert Options to PersistentOptions
|
||||||
|
input.options.mapValuesTo(mutableStateMapOf()) { (_, allPatches) ->
|
||||||
private val selectors by derivedStateOf<Array<Selector>> {
|
allPatches.mapValues { (_, options) -> options.toPersistentMap() }.toPersistentMap()
|
||||||
arrayOf(
|
}
|
||||||
// Patches that were explicitly selected
|
|
||||||
{ bundle, patch ->
|
|
||||||
explicitPatchesSelection[bundle]?.get(patch.name)
|
|
||||||
},
|
|
||||||
// The fallback selection.
|
|
||||||
when (baseSelectionMode) {
|
|
||||||
BaseSelectionMode.DEFAULT -> ({ _, patch -> patch.include })
|
|
||||||
|
|
||||||
BaseSelectionMode.PREVIOUS -> ({ bundle, patch ->
|
|
||||||
previousPatchesSelection[bundle]?.contains(patch.name) ?: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -154,38 +95,39 @@ class PatchesSelectorViewModel(
|
|||||||
|
|
||||||
val compatibleVersions = mutableStateListOf<String>()
|
val compatibleVersions = mutableStateListOf<String>()
|
||||||
|
|
||||||
var filter by mutableStateOf(SHOW_SUPPORTED or SHOW_UNIVERSAL or SHOW_UNSUPPORTED)
|
var filter by mutableIntStateOf(SHOW_SUPPORTED or SHOW_UNIVERSAL or SHOW_UNSUPPORTED)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private suspend fun loadPreviousSelection() {
|
private suspend fun generateDefaultSelection(): PersistentPatchesSelection {
|
||||||
val selection = (input.patchesSelection ?: selectionRepository.getSelection(
|
val bundles = bundlesFlow.first()
|
||||||
packageName
|
val generatedSelection =
|
||||||
)).mapValues { (_, value) -> value.toSet() }
|
bundles.toPatchSelection(allowExperimental) { _, patch -> patch.include }
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
return generatedSelection.toPersistentPatchesSelection()
|
||||||
previousPatchesSelection.putAll(selection)
|
}
|
||||||
|
|
||||||
|
fun selectionIsValid(bundles: List<BundleInfo>) = bundles.any { bundle ->
|
||||||
|
bundle.patchSequence(allowExperimental).any { patch ->
|
||||||
|
isSelected(bundle.uid, patch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun switchBaseSelectionMode() = viewModelScope.launch {
|
fun isSelected(bundle: Int, patch: PatchInfo) = customPatchesSelection?.let { selection ->
|
||||||
baseSelectionMode = if (baseSelectionMode == BaseSelectionMode.DEFAULT) {
|
selection[bundle]?.contains(patch.name) ?: false
|
||||||
BaseSelectionMode.PREVIOUS
|
} ?: patch.include
|
||||||
} else {
|
|
||||||
BaseSelectionMode.DEFAULT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getOrCreateSelection(bundle: Int) =
|
|
||||||
explicitPatchesSelection.getOrPut(bundle, ::mutableStateMapOf)
|
|
||||||
|
|
||||||
fun isSelected(bundle: Int, patch: PatchInfo) =
|
|
||||||
selectors.firstNotNullOf { fn -> fn(bundle, patch) }
|
|
||||||
|
|
||||||
fun togglePatch(bundle: Int, patch: PatchInfo) {
|
|
||||||
val patches = getOrCreateSelection(bundle)
|
|
||||||
|
|
||||||
|
fun togglePatch(bundle: Int, patch: PatchInfo) = viewModelScope.launch {
|
||||||
hasModifiedSelection = true
|
hasModifiedSelection = true
|
||||||
patches[patch.name] = !isSelected(bundle, patch)
|
|
||||||
|
val selection = customPatchesSelection ?: generateDefaultSelection()
|
||||||
|
val newPatches = selection[bundle]?.let { patches ->
|
||||||
|
if (patch.name in patches)
|
||||||
|
patches.remove(patch.name)
|
||||||
|
else
|
||||||
|
patches.add(patch.name)
|
||||||
|
} ?: persistentSetOf(patch.name)
|
||||||
|
|
||||||
|
customPatchesSelection = selection.put(bundle, newPatches)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun confirmSelectionWarning(dismissPermanently: Boolean) {
|
fun confirmSelectionWarning(dismissPermanently: Boolean) {
|
||||||
@ -207,46 +149,34 @@ class PatchesSelectorViewModel(
|
|||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
patchOptions.clear()
|
patchOptions.clear()
|
||||||
baseSelectionMode = BaseSelectionMode.DEFAULT
|
customPatchesSelection = null
|
||||||
explicitPatchesSelection.clear()
|
|
||||||
hasModifiedSelection = false
|
hasModifiedSelection = false
|
||||||
app.toast(app.getString(R.string.patch_selection_reset_toast))
|
app.toast(app.getString(R.string.patch_selection_reset_toast))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSelection(): PatchesSelection {
|
fun getCustomSelection(): PatchesSelection? {
|
||||||
val bundles = bundlesFlow.first()
|
// Convert persistent collections to standard hash collections because persistent collections are not parcelable.
|
||||||
val removeUnsupported = !allowExperimental.get()
|
|
||||||
|
|
||||||
return bundles.associate { bundle ->
|
return customPatchesSelection?.mapValues { (_, v) -> v.toSet() }
|
||||||
val included =
|
|
||||||
bundle.all.filter { isSelected(bundle.uid, it) }.map { it.name }.toMutableSet()
|
|
||||||
|
|
||||||
if (removeUnsupported) {
|
|
||||||
val unsupported = bundle.unsupported.map { it.name }.toSet()
|
|
||||||
included.removeAll(unsupported)
|
|
||||||
}
|
|
||||||
|
|
||||||
bundle.uid to included
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveSelection(selection: PatchesSelection) =
|
fun getOptions(): Options {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
// Convert the collection for the same reasons as in getCustomSelection()
|
||||||
when {
|
|
||||||
hasModifiedSelection -> selectionRepository.updateSelection(packageName, selection)
|
|
||||||
baseSelectionMode == BaseSelectionMode.DEFAULT -> selectionRepository.clearSelection(
|
|
||||||
packageName
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {}
|
return patchOptions.mapValues { (_, allPatches) -> allPatches.mapValues { (_, options) -> options.toMap() } }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun getOptions(): Options = patchOptions
|
|
||||||
fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
|
fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
|
||||||
|
|
||||||
fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {
|
fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {
|
||||||
patchOptions.getOrCreate(bundle).getOrCreate(patch.name)[key] = value
|
// All patches
|
||||||
|
val patchesToOpts = patchOptions.getOrElse(bundle, ::persistentMapOf)
|
||||||
|
// The key-value options of an individual patch
|
||||||
|
val patchToOpts = patchesToOpts
|
||||||
|
.getOrElse(patch.name, ::persistentMapOf)
|
||||||
|
.put(key, value)
|
||||||
|
|
||||||
|
patchOptions[bundle] = patchesToOpts.put(patch.name, patchToOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetOptions(bundle: Int, patch: PatchInfo) {
|
fun resetOptions(bundle: Int, patch: PatchInfo) {
|
||||||
@ -260,7 +190,7 @@ class PatchesSelectorViewModel(
|
|||||||
|
|
||||||
fun openUnsupportedDialog(unsupportedPatches: List<PatchInfo>) {
|
fun openUnsupportedDialog(unsupportedPatches: List<PatchInfo>) {
|
||||||
compatibleVersions.addAll(unsupportedPatches.flatMap { patch ->
|
compatibleVersions.addAll(unsupportedPatches.flatMap { patch ->
|
||||||
patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName }?.versions.orEmpty()
|
patch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,50 +203,28 @@ class PatchesSelectorViewModel(
|
|||||||
const val SHOW_UNIVERSAL = 2 // 2^1
|
const val SHOW_UNIVERSAL = 2 // 2^1
|
||||||
const val SHOW_UNSUPPORTED = 4 // 2^2
|
const val SHOW_UNSUPPORTED = 4 // 2^2
|
||||||
|
|
||||||
private fun <K, K2, V> SnapshotStateMap<K, SnapshotStateMap<K2, V>>.getOrCreate(key: K) =
|
private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver(
|
||||||
getOrPut(key, ::mutableStateMapOf)
|
|
||||||
|
|
||||||
private val optionsSaver: Saver<SnapshotOptions, Options> = snapshotStateMapSaver(
|
|
||||||
// Patch name -> Options
|
// Patch name -> Options
|
||||||
valueSaver = snapshotStateMapSaver(
|
valueSaver = persistentMapSaver(
|
||||||
// Option key -> Option value
|
// Option key -> Option value
|
||||||
valueSaver = snapshotStateMapSaver()
|
valueSaver = persistentMapSaver()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
private val explicitPatchesSelectionSaver: Saver<SnapshotExplicitPatchesSelection, ExplicitPatchesSelection> =
|
private val patchesSaver: Saver<PersistentPatchesSelection?, Nullable<PatchesSelection>> =
|
||||||
snapshotStateMapSaver(valueSaver = snapshotStateMapSaver())
|
nullableSaver(persistentMapSaver(valueSaver = persistentSetSaver()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
data class Params(
|
||||||
* An enum for controlling the behavior of the selector.
|
val app: SelectedApp,
|
||||||
*/
|
val currentSelection: PatchesSelection?,
|
||||||
enum class BaseSelectionMode {
|
val options: Options,
|
||||||
/**
|
|
||||||
* Selection is determined by the [PatchInfo.include] field.
|
|
||||||
*/
|
|
||||||
DEFAULT,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Selection is determined by what the user selected previously.
|
|
||||||
* Any patch that is not part of the previous selection will be deselected.
|
|
||||||
*/
|
|
||||||
PREVIOUS
|
|
||||||
}
|
|
||||||
|
|
||||||
data class BundleInfo(
|
|
||||||
val name: String,
|
|
||||||
val uid: Int,
|
|
||||||
val all: List<PatchInfo>,
|
|
||||||
val supported: List<PatchInfo>,
|
|
||||||
val unsupported: List<PatchInfo>,
|
|
||||||
val universal: List<PatchInfo>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private typealias Selector = (Int, PatchInfo) -> Boolean?
|
// Versions of other types, but utilizing persistent/observable collection types.
|
||||||
private typealias ExplicitPatchesSelection = Map<Int, Map<String, Boolean>>
|
private typealias PersistentOptions = SnapshotStateMap<Int, PersistentMap<String, PersistentMap<String, Any?>>>
|
||||||
|
private typealias PersistentPatchesSelection = PersistentMap<Int, PersistentSet<String>>
|
||||||
|
|
||||||
// Versions of other types, but utilizing observable collection types instead.
|
private fun PatchesSelection.toPersistentPatchesSelection(): PersistentPatchesSelection =
|
||||||
private typealias SnapshotOptions = SnapshotStateMap<Int, SnapshotStateMap<String, SnapshotStateMap<String, Any?>>>
|
mapValues { (_, v) -> v.toPersistentSet() }.toPersistentMap()
|
||||||
private typealias SnapshotExplicitPatchesSelection = SnapshotStateMap<Int, SnapshotStateMap<String, Boolean>>
|
|
@ -0,0 +1,186 @@
|
|||||||
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||||
|
import androidx.lifecycle.viewmodel.compose.saveable
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||||
|
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||||
|
import app.revanced.manager.ui.model.BundleInfo
|
||||||
|
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
||||||
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
|
import app.revanced.manager.util.Options
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import app.revanced.manager.util.PatchesSelection
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
|
|
||||||
|
@OptIn(SavedStateHandleSaveableApi::class)
|
||||||
|
class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||||
|
val bundlesRepo: PatchBundleRepository = get()
|
||||||
|
private val selectionRepository: PatchSelectionRepository = get()
|
||||||
|
private val optionsRepository: PatchOptionsRepository = get()
|
||||||
|
private val pm: PM = get()
|
||||||
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
|
val prefs: PreferencesManager = get()
|
||||||
|
|
||||||
|
private val persistConfiguration = input.patches == null
|
||||||
|
|
||||||
|
private var _selectedApp by savedStateHandle.saveable {
|
||||||
|
mutableStateOf(input.app)
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedApp
|
||||||
|
get() = _selectedApp
|
||||||
|
set(value) {
|
||||||
|
_selectedApp = value
|
||||||
|
invalidateSelectedAppInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedAppInfo: PackageInfo? by mutableStateOf(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
invalidateSelectedAppInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
var options: Options by savedStateHandle.saveable {
|
||||||
|
val state = mutableStateOf<Options>(emptyMap())
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||||
|
|
||||||
|
val packageName = selectedApp.packageName // Accessing this from another thread may cause crashes.
|
||||||
|
state.value = withContext(Dispatchers.Default) { optionsRepository.getOptions(packageName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
state
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var selectionState by savedStateHandle.saveable {
|
||||||
|
if (input.patches != null) {
|
||||||
|
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
|
||||||
|
}
|
||||||
|
|
||||||
|
val selection: MutableState<SelectionState> = mutableStateOf(SelectionState.Default)
|
||||||
|
|
||||||
|
// Get previous selection (if present).
|
||||||
|
viewModelScope.launch {
|
||||||
|
val previous = selectionRepository.getSelection(selectedApp.packageName)
|
||||||
|
|
||||||
|
if (previous.values.sumOf { it.size } == 0) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.value = SelectionState.Customized(previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
selection
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
|
||||||
|
val info = when (val app = selectedApp) {
|
||||||
|
is SelectedApp.Download -> null
|
||||||
|
is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) }
|
||||||
|
is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedAppInfo = info
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles)
|
||||||
|
|
||||||
|
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||||
|
selectionState.patches(bundles, allowUnsupported)
|
||||||
|
|
||||||
|
fun getCustomPatches(
|
||||||
|
bundles: List<BundleInfo>,
|
||||||
|
allowUnsupported: Boolean
|
||||||
|
): PatchesSelection? =
|
||||||
|
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
|
||||||
|
|
||||||
|
fun updateConfiguration(
|
||||||
|
selection: PatchesSelection?,
|
||||||
|
options: Options,
|
||||||
|
bundles: List<BundleInfo>
|
||||||
|
) {
|
||||||
|
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
||||||
|
|
||||||
|
val filteredOptions = options.filtered(bundles)
|
||||||
|
this.options = filteredOptions
|
||||||
|
|
||||||
|
if (!persistConfiguration) return
|
||||||
|
|
||||||
|
val packageName = selectedApp.packageName
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
||||||
|
?: selectionRepository.clearSelection(packageName)
|
||||||
|
|
||||||
|
optionsRepository.saveOptions(packageName, filteredOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val app: SelectedApp,
|
||||||
|
val patches: PatchesSelection?,
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
/**
|
||||||
|
* Returns a copy with all nonexistent options removed.
|
||||||
|
*/
|
||||||
|
private fun Options.filtered(bundles: List<BundleInfo>): Options = buildMap options@{
|
||||||
|
bundles.forEach bundles@{ bundle ->
|
||||||
|
val bundleOptions = this@filtered[bundle.uid] ?: return@bundles
|
||||||
|
|
||||||
|
val patches = bundle.all.associateBy { it.name }
|
||||||
|
|
||||||
|
this@options[bundle.uid] = buildMap bundleOptions@{
|
||||||
|
bundleOptions.forEach patch@{ (patchName, values) ->
|
||||||
|
// Get all valid option keys for the patch.
|
||||||
|
val validOptionKeys =
|
||||||
|
patches[patchName]?.options?.map { it.key }?.toSet() ?: return@patch
|
||||||
|
|
||||||
|
this@bundleOptions[patchName] = values.filterKeys { key ->
|
||||||
|
key in validOptionKeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed interface SelectionState : Parcelable {
|
||||||
|
fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean): PatchesSelection
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Customized(val patchesSelection: PatchesSelection) : SelectionState {
|
||||||
|
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||||
|
bundles.toPatchSelection(
|
||||||
|
allowUnsupported
|
||||||
|
) { uid, patch ->
|
||||||
|
patchesSelection[uid]?.contains(patch.name) ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object Default : SelectionState {
|
||||||
|
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||||
|
bundles.toPatchSelection(allowUnsupported) { _, patch -> patch.include }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,75 +0,0 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.network.api.ReVancedAPI
|
|
||||||
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
|
|
||||||
import app.revanced.manager.network.service.HttpService
|
|
||||||
import app.revanced.manager.network.utils.getOrThrow
|
|
||||||
import app.revanced.manager.util.APK_MIMETYPE
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import app.revanced.manager.util.PM
|
|
||||||
import app.revanced.manager.util.uiSafe
|
|
||||||
import io.ktor.client.plugins.onDownload
|
|
||||||
import io.ktor.client.request.url
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class UpdateProgressViewModel(
|
|
||||||
app: Application,
|
|
||||||
private val reVancedAPI: ReVancedAPI,
|
|
||||||
private val http: HttpService,
|
|
||||||
private val pm: PM
|
|
||||||
) : ViewModel() {
|
|
||||||
var downloadedSize by mutableStateOf(0L)
|
|
||||||
private set
|
|
||||||
var totalSize by mutableStateOf(0L)
|
|
||||||
private set
|
|
||||||
val downloadProgress by derivedStateOf {
|
|
||||||
if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f
|
|
||||||
|
|
||||||
downloadedSize.toFloat() / totalSize.toFloat()
|
|
||||||
}
|
|
||||||
val isInstalling by derivedStateOf { downloadProgress >= 1 }
|
|
||||||
var finished by mutableStateOf(false)
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val location = File.createTempFile("updater", ".apk", app.cacheDir)
|
|
||||||
private val job = viewModelScope.launch {
|
|
||||||
uiSafe(app, R.string.download_manager_failed, "Failed to download manager") {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val asset = reVancedAPI
|
|
||||||
.getRelease("revanced-manager")
|
|
||||||
.getOrThrow()
|
|
||||||
.findAssetByType(APK_MIMETYPE)
|
|
||||||
|
|
||||||
http.download(location) {
|
|
||||||
url(asset.downloadUrl)
|
|
||||||
onDownload { bytesSentTotal, contentLength ->
|
|
||||||
downloadedSize = bytesSentTotal
|
|
||||||
totalSize = contentLength
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finished = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun installUpdate() = viewModelScope.launch {
|
|
||||||
pm.installApp(listOf(location))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
location.delete()
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,170 @@
|
|||||||
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.data.platform.NetworkInfo
|
||||||
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
|
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
|
||||||
|
import app.revanced.manager.network.dto.ReVancedRelease
|
||||||
|
import app.revanced.manager.network.service.HttpService
|
||||||
|
import app.revanced.manager.network.utils.getOrThrow
|
||||||
|
import app.revanced.manager.service.InstallService
|
||||||
|
import app.revanced.manager.service.UninstallService
|
||||||
|
import app.revanced.manager.util.APK_MIMETYPE
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import app.revanced.manager.util.simpleMessage
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import app.revanced.manager.util.toast
|
||||||
|
import app.revanced.manager.util.uiSafe
|
||||||
|
import io.ktor.client.plugins.onDownload
|
||||||
|
import io.ktor.client.request.url
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class UpdateViewModel(
|
||||||
|
private val downloadOnScreenEntry: Boolean
|
||||||
|
) : ViewModel(), KoinComponent {
|
||||||
|
private val app: Application by inject()
|
||||||
|
private val reVancedAPI: ReVancedAPI by inject()
|
||||||
|
private val http: HttpService by inject()
|
||||||
|
private val pm: PM by inject()
|
||||||
|
private val networkInfo: NetworkInfo by inject()
|
||||||
|
|
||||||
|
var downloadedSize by mutableStateOf(0L)
|
||||||
|
private set
|
||||||
|
var totalSize by mutableStateOf(0L)
|
||||||
|
private set
|
||||||
|
val downloadProgress by derivedStateOf {
|
||||||
|
if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f
|
||||||
|
|
||||||
|
downloadedSize.toFloat() / totalSize.toFloat()
|
||||||
|
}
|
||||||
|
var showInternetCheckDialog by mutableStateOf(false)
|
||||||
|
var state by mutableStateOf(State.CAN_DOWNLOAD)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var installError by mutableStateOf("")
|
||||||
|
|
||||||
|
var changelog: Changelog? by mutableStateOf(null)
|
||||||
|
|
||||||
|
private val location = File.createTempFile("updater", ".apk", app.cacheDir)
|
||||||
|
private var release: ReVancedRelease? = null
|
||||||
|
private val job = viewModelScope.launch {
|
||||||
|
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val response = reVancedAPI
|
||||||
|
.getLatestRelease("revanced-manager")
|
||||||
|
.getOrThrow()
|
||||||
|
release = response
|
||||||
|
changelog = Changelog(
|
||||||
|
response.metadata.tag,
|
||||||
|
response.findAssetByType(APK_MIMETYPE).downloadCount,
|
||||||
|
response.metadata.publishedAt,
|
||||||
|
response.metadata.body
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (downloadOnScreenEntry) {
|
||||||
|
downloadUpdate()
|
||||||
|
} else {
|
||||||
|
state = State.CAN_DOWNLOAD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadUpdate(ignoreInternetCheck: Boolean = false) = viewModelScope.launch {
|
||||||
|
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (!networkInfo.isSafe() && !ignoreInternetCheck) {
|
||||||
|
showInternetCheckDialog = true
|
||||||
|
} else {
|
||||||
|
state = State.DOWNLOADING
|
||||||
|
val asset = release?.findAssetByType(APK_MIMETYPE)
|
||||||
|
?: throw Exception("couldn't find asset to download")
|
||||||
|
|
||||||
|
http.download(location) {
|
||||||
|
url(asset.downloadUrl)
|
||||||
|
onDownload { bytesSentTotal, contentLength ->
|
||||||
|
downloadedSize = bytesSentTotal
|
||||||
|
totalSize = contentLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state = State.CAN_INSTALL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installUpdate() = viewModelScope.launch {
|
||||||
|
state = State.INSTALLING
|
||||||
|
|
||||||
|
pm.installApp(listOf(location))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
intent?.let {
|
||||||
|
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
||||||
|
val extra =
|
||||||
|
intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
|
||||||
|
|
||||||
|
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||||
|
app.toast(app.getString(R.string.install_app_success))
|
||||||
|
state = State.SUCCESS
|
||||||
|
} else {
|
||||||
|
state = State.FAILED
|
||||||
|
// TODO: handle install fail with a popup
|
||||||
|
installError = extra
|
||||||
|
app.toast(app.getString(R.string.install_app_fail, extra))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
|
||||||
|
addAction(InstallService.APP_INSTALL_ACTION)
|
||||||
|
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
app.unregisterReceiver(installBroadcastReceiver)
|
||||||
|
|
||||||
|
job.cancel()
|
||||||
|
location.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Changelog(
|
||||||
|
val version: String,
|
||||||
|
val downloadCount: Int,
|
||||||
|
val publishDate: String,
|
||||||
|
val body: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class State(@StringRes val title: Int, val showCancel: Boolean = false) {
|
||||||
|
CAN_DOWNLOAD(R.string.update_available),
|
||||||
|
DOWNLOADING(R.string.downloading_manager_update, true),
|
||||||
|
CAN_INSTALL(R.string.ready_to_install_update, true),
|
||||||
|
INSTALLING(R.string.installing_manager_update),
|
||||||
|
FAILED(R.string.install_update_manager_failed),
|
||||||
|
SUCCESS(R.string.update_completed)
|
||||||
|
}
|
||||||
|
}
|
@ -68,7 +68,7 @@ class VersionSelectorViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val downloadedVersions = downloadedAppRepository.getAll().map { downloadedApps ->
|
val downloadedVersions = downloadedAppRepository.getAll().map { downloadedApps ->
|
||||||
downloadedApps.filter { it.packageName == packageName }.map { SelectedApp.Local(it.packageName, it.version, it.file, false) }
|
downloadedApps.filter { it.packageName == packageName }.map { SelectedApp.Local(it.packageName, it.version, downloadedAppRepository.getApkFileForApp(it), false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -98,7 +98,18 @@ class PM(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPackageInfo(file: File): PackageInfo? = app.packageManager.getPackageArchiveInfo(file.absolutePath, 0)
|
fun getPackageInfo(file: File): PackageInfo? {
|
||||||
|
val path = file.absolutePath
|
||||||
|
val pkgInfo = app.packageManager.getPackageArchiveInfo(path, 0) ?: return null
|
||||||
|
|
||||||
|
// This is needed in order to load label and icon.
|
||||||
|
pkgInfo.applicationInfo.apply {
|
||||||
|
sourceDir = path
|
||||||
|
publicSourceDir = path
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkgInfo
|
||||||
|
}
|
||||||
|
|
||||||
fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString()
|
fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString()
|
||||||
|
|
||||||
|
@ -2,6 +2,12 @@ package app.revanced.manager.util
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.icu.number.Notation
|
||||||
|
import android.icu.number.NumberFormatter
|
||||||
|
import android.icu.number.Precision
|
||||||
|
import android.icu.text.CompactDecimalFormat
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@ -11,17 +17,28 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import app.revanced.manager.R
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
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 java.time.Duration
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.DateTimeParseException
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
typealias PatchesSelection = Map<Int, Set<String>>
|
typealias PatchesSelection = Map<Int, Set<String>>
|
||||||
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
|
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
|
||||||
|
|
||||||
|
val Context.isDebuggable get() = 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
|
||||||
|
|
||||||
fun Context.openUrl(url: String) {
|
fun Context.openUrl(url: String) {
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
|
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
@ -41,16 +58,21 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
|||||||
* @param logMsg The log message.
|
* @param logMsg The log message.
|
||||||
* @param block The code to execute.
|
* @param block The code to execute.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
||||||
try {
|
try {
|
||||||
block()
|
block()
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
context.toast(
|
// You can only toast on the main thread.
|
||||||
context.getString(
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
toastMsg,
|
context.toast(
|
||||||
error.simpleMessage()
|
context.getString(
|
||||||
|
toastMsg,
|
||||||
|
error.simpleMessage()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
Log.e(tag, logMsg, error)
|
Log.e(tag, logMsg, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,4 +119,50 @@ suspend fun <T> Flow<Iterable<T>>.collectEach(block: suspend (T) -> Unit) {
|
|||||||
block(it)
|
block(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.formatNumber(): String {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
NumberFormatter.with()
|
||||||
|
.notation(Notation.compactShort())
|
||||||
|
.decimal(NumberFormatter.DecimalSeparatorDisplay.ALWAYS)
|
||||||
|
.precision(Precision.fixedFraction(1))
|
||||||
|
.locale(Locale.getDefault())
|
||||||
|
.format(this)
|
||||||
|
.toString()
|
||||||
|
} else {
|
||||||
|
val compact = CompactDecimalFormat.getInstance(
|
||||||
|
Locale.getDefault(), CompactDecimalFormat.CompactStyle.SHORT
|
||||||
|
)
|
||||||
|
compact.maximumFractionDigits = 1
|
||||||
|
compact.format(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.relativeTime(context: Context): String {
|
||||||
|
try {
|
||||||
|
val currentTime = ZonedDateTime.now(ZoneId.of("UTC"))
|
||||||
|
val inputDateTime = ZonedDateTime.parse(this)
|
||||||
|
val duration = Duration.between(inputDateTime, currentTime)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
duration.toMinutes() < 1 -> context.getString(R.string.just_now)
|
||||||
|
duration.toMinutes() < 60 -> context.getString(R.string.minutes_ago, duration.toMinutes().toString())
|
||||||
|
duration.toHours() < 24 -> context.getString(R.string.hours_ago, duration.toHours().toString())
|
||||||
|
duration.toDays() < 30 -> context.getString(R.string.days_ago, duration.toDays().toString())
|
||||||
|
else -> {
|
||||||
|
val formatter = DateTimeFormatter.ofPattern("MMM d")
|
||||||
|
val formattedDate = inputDateTime.format(formatter)
|
||||||
|
if (inputDateTime.year != currentTime.year) {
|
||||||
|
val yearFormatter = DateTimeFormatter.ofPattern(", yyyy")
|
||||||
|
val formattedYear = inputDateTime.format(yearFormatter)
|
||||||
|
"$formattedDate$formattedYear"
|
||||||
|
} else {
|
||||||
|
formattedDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: DateTimeParseException) {
|
||||||
|
return context.getString(R.string.invalid_date)
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package app.revanced.manager.util.saver
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.parcelize.RawValue
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class Nullable<T>(val inner: @RawValue T?) : Parcelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a saver that can save nullable versions of types that have custom savers.
|
||||||
|
*/
|
||||||
|
fun <Original : Any, Saveable : Any> nullableSaver(baseSaver: Saver<Original, Saveable>): Saver<Original?, Nullable<Saveable>> =
|
||||||
|
Saver(
|
||||||
|
save = { value ->
|
||||||
|
with(baseSaver) {
|
||||||
|
save(value ?: return@Saver Nullable(null))
|
||||||
|
}?.let(::Nullable)
|
||||||
|
},
|
||||||
|
restore = {
|
||||||
|
it.inner?.let(baseSaver::restore)
|
||||||
|
}
|
||||||
|
)
|
@ -0,0 +1,69 @@
|
|||||||
|
package app.revanced.manager.util.saver
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import kotlinx.collections.immutable.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [Saver] for [PersistentList]s.
|
||||||
|
*/
|
||||||
|
fun <T> persistentListSaver() = Saver<PersistentList<T>, List<T>>(
|
||||||
|
save = {
|
||||||
|
it.toList()
|
||||||
|
},
|
||||||
|
restore = {
|
||||||
|
it.toPersistentList()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [Saver] for [PersistentSet]s.
|
||||||
|
*/
|
||||||
|
fun <T> persistentSetSaver() = Saver<PersistentSet<T>, Set<T>>(
|
||||||
|
save = {
|
||||||
|
it.toSet()
|
||||||
|
},
|
||||||
|
restore = {
|
||||||
|
it.toPersistentSet()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [Saver] for [PersistentMap]s.
|
||||||
|
*/
|
||||||
|
fun <K, V> persistentMapSaver() = Saver<PersistentMap<K, V>, Map<K, V>>(
|
||||||
|
save = {
|
||||||
|
it.toMap()
|
||||||
|
},
|
||||||
|
restore = {
|
||||||
|
it.toPersistentMap()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a saver for [PersistentMap]s with a custom [Saver] used for the values.
|
||||||
|
* Null values will not be saved by this [Saver].
|
||||||
|
*
|
||||||
|
* @param valueSaver The [Saver] used for the values of the [Map].
|
||||||
|
*/
|
||||||
|
fun <K, Original, Saveable : Any> persistentMapSaver(
|
||||||
|
valueSaver: Saver<Original, Saveable>
|
||||||
|
) = Saver<PersistentMap<K, Original>, Map<K, Saveable>>(
|
||||||
|
save = {
|
||||||
|
buildMap {
|
||||||
|
it.forEach { (key, value) ->
|
||||||
|
with(valueSaver) {
|
||||||
|
save(value)?.let {
|
||||||
|
this@buildMap[key] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restore = {
|
||||||
|
buildMap {
|
||||||
|
it.forEach { (key, value) ->
|
||||||
|
this[key] = valueSaver.restore(value) ?: return@forEach
|
||||||
|
}
|
||||||
|
}.toPersistentMap()
|
||||||
|
}
|
||||||
|
)
|
@ -11,6 +11,8 @@
|
|||||||
<string name="select_app">Select an app</string>
|
<string name="select_app">Select an app</string>
|
||||||
<string name="select_patches">Select patches</string>
|
<string name="select_patches">Select patches</string>
|
||||||
|
|
||||||
|
<string name="unsupported_architecture_warning">Patching on ARMv7 devices is not yet supported and will most likely fail.</string>
|
||||||
|
|
||||||
<string name="import_">Import</string>
|
<string name="import_">Import</string>
|
||||||
<string name="import_bundle">Import patch bundle</string>
|
<string name="import_bundle">Import patch bundle</string>
|
||||||
<string name="bundle_patches">Bundle patches</string>
|
<string name="bundle_patches">Bundle patches</string>
|
||||||
@ -23,7 +25,19 @@
|
|||||||
|
|
||||||
<string name="bundle_missing">Missing</string>
|
<string name="bundle_missing">Missing</string>
|
||||||
<string name="bundle_error">Error</string>
|
<string name="bundle_error">Error</string>
|
||||||
|
|
||||||
|
<string name="selected_app_meta">%1s • %2d available patches</string>
|
||||||
|
|
||||||
|
<string name="patch_item_description">Start patching the application</string>
|
||||||
|
<string name="patch_selector_item">Patch selection and options</string>
|
||||||
|
<string name="patch_selector_item_description">%d patches selected</string>
|
||||||
|
<string name="no_patches_selected">No patches selected</string>
|
||||||
|
|
||||||
|
<string name="version_selector_item">Change version</string>
|
||||||
|
<string name="version_selector_item_description">%s selected</string>
|
||||||
|
|
||||||
|
<string name="legacy_import_failed">Could not import legacy settings</string>
|
||||||
|
|
||||||
<string name="auto_updates_dialog_title">Select updates to receive</string>
|
<string name="auto_updates_dialog_title">Select updates to receive</string>
|
||||||
<string name="auto_updates_dialog_description">Periodically connect to update providers to check for updates.</string>
|
<string name="auto_updates_dialog_description">Periodically connect to update providers to check for updates.</string>
|
||||||
<string name="auto_updates_dialog_manager">ReVanced Manager</string>
|
<string name="auto_updates_dialog_manager">ReVanced Manager</string>
|
||||||
@ -51,6 +65,8 @@
|
|||||||
<string name="dynamic_color_description">Adapt colors to the wallpaper</string>
|
<string name="dynamic_color_description">Adapt colors to the wallpaper</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="multithreaded_dex_file_writer">Multi-threaded DEX file writer</string>
|
||||||
|
<string name="multithreaded_dex_file_writer_description">Use multiple cores to write DEX files. This is faster, but uses more memory</string>
|
||||||
<string name="experimental_patches">Allow experimental patches</string>
|
<string name="experimental_patches">Allow experimental patches</string>
|
||||||
<string name="experimental_patches_description">Allow patching incompatible patches with experimental versions, something may break</string>
|
<string name="experimental_patches_description">Allow patching incompatible patches with experimental versions, something may break</string>
|
||||||
<string name="import_keystore">Import keystore</string>
|
<string name="import_keystore">Import keystore</string>
|
||||||
@ -76,6 +92,13 @@
|
|||||||
<string name="backup_patches_selection_fail">Failed to backup patches selection: %s</string>
|
<string name="backup_patches_selection_fail">Failed to backup patches selection: %s</string>
|
||||||
<string name="clear_patches_selection">Clear patches selection</string>
|
<string name="clear_patches_selection">Clear patches selection</string>
|
||||||
<string name="clear_patches_selection_description">Clear all patches selection</string>
|
<string name="clear_patches_selection_description">Clear all patches selection</string>
|
||||||
|
<string name="patch_options">Patch options</string>
|
||||||
|
<string name="patch_options_clear_package">Clear patch options for package</string>
|
||||||
|
<string name="patch_options_clear_package_description">Resets patch options for a single package</string>
|
||||||
|
<string name="patch_options_clear_bundle">Clear patch options for bundle</string>
|
||||||
|
<string name="patch_options_clear_bundle_description">Resets patch options for all patches in a bundle</string>
|
||||||
|
<string name="patch_options_clear_all">Clear all patch options</string>
|
||||||
|
<string name="patch_options_clear_all_description">Resets all patch options</string>
|
||||||
<string name="prefer_splits">Prefer split apks</string>
|
<string name="prefer_splits">Prefer split apks</string>
|
||||||
<string name="prefer_splits_description">Prefer split apks instead of full apks</string>
|
<string name="prefer_splits_description">Prefer split apks instead of full apks</string>
|
||||||
<string name="prefer_universal">Prefer universal apks</string>
|
<string name="prefer_universal">Prefer universal apks</string>
|
||||||
@ -143,8 +166,6 @@
|
|||||||
<string name="unsupported_app">Unsupported app</string>
|
<string name="unsupported_app">Unsupported app</string>
|
||||||
<string name="unsupported_patches">Unsupported patches</string>
|
<string name="unsupported_patches">Unsupported patches</string>
|
||||||
<string name="universal_patches">Universal patches</string>
|
<string name="universal_patches">Universal patches</string>
|
||||||
<string name="menu_opt_selection_mode_default">Use default selection</string>
|
|
||||||
<string name="menu_opt_selection_mode_previous">Use previous selection</string>
|
|
||||||
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
|
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
|
||||||
<string name="selection_warning_title">Stop using defaults?</string>
|
<string name="selection_warning_title">Stop using defaults?</string>
|
||||||
<string name="selection_warning_description">You may encounter issues when not using the default patch selection and options.</string>
|
<string name="selection_warning_description">You may encounter issues when not using the default patch selection and options.</string>
|
||||||
@ -152,6 +173,7 @@
|
|||||||
<string name="supported">Supported</string>
|
<string name="supported">Supported</string>
|
||||||
<string name="universal">Universal</string>
|
<string name="universal">Universal</string>
|
||||||
<string name="unsupported">Unsupported</string>
|
<string name="unsupported">Unsupported</string>
|
||||||
|
<string name="search_patches">Patch name</string>
|
||||||
<string name="app_not_supported">Some of the patches do not support this app version (%1$s). The patches only support the following version(s): %2$s.</string>
|
<string name="app_not_supported">Some of the patches do not support this app version (%1$s). The patches only support the following version(s): %2$s.</string>
|
||||||
<string name="continue_with_version">Continue with this version?</string>
|
<string name="continue_with_version">Continue with this version?</string>
|
||||||
<string name="version_not_supported">Not all patches support this version (%s). Do you want to continue anyway?</string>
|
<string name="version_not_supported">Not all patches support this version (%s). Do you want to continue anyway?</string>
|
||||||
@ -188,6 +210,9 @@
|
|||||||
<string name="downloadable_versions">Downloadable versions</string>
|
<string name="downloadable_versions">Downloadable versions</string>
|
||||||
<string name="already_patched">Already patched</string>
|
<string name="already_patched">Already patched</string>
|
||||||
|
|
||||||
|
<string name="patches_selector_sheet_filter_title">Filter</string>
|
||||||
|
<string name="patches_selector_sheet_filter_compat_title">Compatibility</string>
|
||||||
|
|
||||||
<string name="string_option_icon_description">Edit</string>
|
<string name="string_option_icon_description">Edit</string>
|
||||||
<string name="string_option_menu_description">More options</string>
|
<string name="string_option_menu_description">More options</string>
|
||||||
<string name="string_option_placeholder">Value</string>
|
<string name="string_option_placeholder">Value</string>
|
||||||
@ -206,8 +231,8 @@
|
|||||||
<string name="install_app_fail">Failed to install app: %s</string>
|
<string name="install_app_fail">Failed to install app: %s</string>
|
||||||
<string name="uninstall_app_fail">Failed to uninstall app: %s</string>
|
<string name="uninstall_app_fail">Failed to uninstall app: %s</string>
|
||||||
<string name="open_app">Open</string>
|
<string name="open_app">Open</string>
|
||||||
<string name="export_app">Export</string>
|
<string name="save_apk">Save APK</string>
|
||||||
<string name="export_app_success">Apk exported</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="select_install_type">Select installation type</string>
|
<string name="select_install_type">Select installation type</string>
|
||||||
@ -219,6 +244,7 @@
|
|||||||
<string name="patcher_step_group_patching">Patching</string>
|
<string name="patcher_step_group_patching">Patching</string>
|
||||||
<string name="patcher_step_group_saving">Saving</string>
|
<string name="patcher_step_group_saving">Saving</string>
|
||||||
<string name="patcher_step_write_patched">Write patched Apk</string>
|
<string name="patcher_step_write_patched">Write patched Apk</string>
|
||||||
|
<string name="patcher_step_sign_apk">Sign Apk</string>
|
||||||
<string name="patcher_notification_message">Patching in progress…</string>
|
<string name="patcher_notification_message">Patching in progress…</string>
|
||||||
|
|
||||||
<string name="step_completed">completed</string>
|
<string name="step_completed">completed</string>
|
||||||
@ -230,6 +256,7 @@
|
|||||||
|
|
||||||
<string name="more">More</string>
|
<string name="more">More</string>
|
||||||
<string name="continue_">Continue</string>
|
<string name="continue_">Continue</string>
|
||||||
|
<string name="dismiss">Dismiss</string>
|
||||||
<string name="permanent_dismiss">Do not show this again</string>
|
<string name="permanent_dismiss">Do not show this again</string>
|
||||||
<string name="donate">Donate</string>
|
<string name="donate">Donate</string>
|
||||||
<string name="website">Website</string>
|
<string name="website">Website</string>
|
||||||
@ -248,6 +275,12 @@
|
|||||||
<string name="bundle_type_description">Choose the type of bundle you want</string>
|
<string name="bundle_type_description">Choose the type of bundle you want</string>
|
||||||
<string name="about_revanced_manager">About ReVanced Manager</string>
|
<string name="about_revanced_manager">About ReVanced Manager</string>
|
||||||
<string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string>
|
<string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string>
|
||||||
|
<string name="update_available">An update is available</string>
|
||||||
|
<string name="current_version">Current version: %s</string>
|
||||||
|
<string name="new_version">New version: %s</string>
|
||||||
|
<string name="ready_to_install_update">Ready to install update</string>
|
||||||
|
<string name="update_completed">Update installed</string>
|
||||||
|
<string name="install_update_manager_failed">Failed to install update</string>
|
||||||
<string name="update_notification">A minor update for ReVanced Manager is available. Click here to update and get the latest features and fixes!</string>
|
<string name="update_notification">A minor update for ReVanced Manager is available. Click here to update and get the latest features and fixes!</string>
|
||||||
<string name="update_channel">Update channel</string>
|
<string name="update_channel">Update channel</string>
|
||||||
<string name="update_channel_description">Stable</string>
|
<string name="update_channel_description">Stable</string>
|
||||||
@ -257,7 +290,7 @@
|
|||||||
<string name="changelog_loading">Loading changelog</string>
|
<string name="changelog_loading">Loading changelog</string>
|
||||||
<string name="changelog_download_fail">Failed to download changelog: %s</string>
|
<string name="changelog_download_fail">Failed to download changelog: %s</string>
|
||||||
<string name="changelog_description">Check out the latest changes in this update</string>
|
<string name="changelog_description">Check out the latest changes in this update</string>
|
||||||
<string name="battery_optimization_notification">Battery optimization must be turned off in order for ReVanced Manager to work correctly in the background. Tap here to turn off.</string>
|
<string name="battery_optimization_notification">Battery optimization must be turned off in order for ReVanced Manager to work correctly in the background. Click here to turn off.</string>
|
||||||
<string name="installing_manager_update">Installing update…</string>
|
<string name="installing_manager_update">Installing update…</string>
|
||||||
<string name="downloading_manager_update">Downloading update…</string>
|
<string name="downloading_manager_update">Downloading update…</string>
|
||||||
<string name="download_manager_failed">Failed to download update: %s</string>
|
<string name="download_manager_failed">Failed to download update: %s</string>
|
||||||
@ -265,4 +298,21 @@
|
|||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<string name="update">Update</string>
|
<string name="update">Update</string>
|
||||||
<string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will close when updating.</string>
|
<string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will close when updating.</string>
|
||||||
|
<string name="no_changelogs_found">No changelogs found</string>
|
||||||
|
<string name="just_now">Just now</string>
|
||||||
|
<string name="minutes_ago">%sm ago</string>
|
||||||
|
<string name="hours_ago">%sh ago</string>
|
||||||
|
<string name="days_ago">%sd ago</string>
|
||||||
|
<string name="invalid_date">Invalid date</string>
|
||||||
|
<string name="disable_battery_optimization">Disable battery optimization</string>
|
||||||
|
|
||||||
|
<string name="failed_to_check_updates">Failed to check for updates</string>
|
||||||
|
<string name="dismiss_temporary">Not now</string>
|
||||||
|
<string name="update_available_dialog_title">New update available</string>
|
||||||
|
<string name="update_available_dialog_description">A new version (%s) is available for download.</string>
|
||||||
|
<string name="failed_to_download_update">Failed to download update: %s</string>
|
||||||
|
<string name="download">Download</string>
|
||||||
|
<string name="download_confirmation_metered">You are currently on a metered connection, and data charges from your service provider may apply.\n\nDo you still want to continue?</string>
|
||||||
|
<string name="download_update_confirmation">Download update?</string>
|
||||||
|
<string name="no_contributors_found">No contributors found</string>
|
||||||
</resources>
|
</resources>
|
@ -1,24 +1,25 @@
|
|||||||
[versions]
|
[versions]
|
||||||
ktx = "1.10.1"
|
ktx = "1.12.0"
|
||||||
viewmodel-lifecycle = "2.6.2"
|
viewmodel-lifecycle = "2.6.2"
|
||||||
splash-screen = "1.0.1"
|
splash-screen = "1.0.1"
|
||||||
compose-activity = "1.7.2"
|
compose-activity = "1.8.0"
|
||||||
paging = "3.2.1"
|
paging = "3.2.1"
|
||||||
preferences-datastore = "1.0.0"
|
preferences-datastore = "1.0.0"
|
||||||
work-runtime = "2.8.1"
|
work-runtime = "2.8.1"
|
||||||
compose-bom = "2023.06.01"
|
compose-bom = "2023.10.00"
|
||||||
accompanist = "0.30.1"
|
accompanist = "0.30.1"
|
||||||
serialization = "1.6.0"
|
serialization = "1.6.0"
|
||||||
collection = "0.3.5"
|
collection = "0.3.5"
|
||||||
room-version = "2.5.2"
|
room-version = "2.5.2"
|
||||||
revanced-patcher = "16.0.1"
|
revanced-patcher = "19.1.0"
|
||||||
revanced-library = "1.1.1"
|
revanced-library = "1.4.0"
|
||||||
koin-version = "3.4.3"
|
koin-version = "3.4.3"
|
||||||
koin-version-compose = "3.4.6"
|
koin-version-compose = "3.4.6"
|
||||||
reimagined-navigation = "1.4.0"
|
reimagined-navigation = "1.5.0"
|
||||||
ktor = "2.3.3"
|
ktor = "2.3.3"
|
||||||
markdown = "0.5.0"
|
markdown-renderer = "0.8.0"
|
||||||
androidGradlePlugin = "8.1.1"
|
fading-edges = "1.0.4"
|
||||||
|
androidGradlePlugin = "8.1.2"
|
||||||
kotlinGradlePlugin = "1.9.10"
|
kotlinGradlePlugin = "1.9.10"
|
||||||
devToolsGradlePlugin = "1.9.10-1.0.13"
|
devToolsGradlePlugin = "1.9.10-1.0.13"
|
||||||
aboutLibrariesGradlePlugin = "10.8.3"
|
aboutLibrariesGradlePlugin = "10.8.3"
|
||||||
@ -43,7 +44,7 @@ compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref =
|
|||||||
compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
compose-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
|
compose-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
|
||||||
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
compose-material3 = { group = "androidx.compose.material3", name = "material3", version = "1.2.0-alpha10"}
|
||||||
compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||||
|
|
||||||
# Coil
|
# Coil
|
||||||
@ -93,7 +94,10 @@ skrapeit-dsl = { group = "it.skrape", name = "skrapeit-dsl", version.ref = "skra
|
|||||||
skrapeit-parser = { group = "it.skrape", name = "skrapeit-html-parser", version.ref = "skrapeit" }
|
skrapeit-parser = { group = "it.skrape", name = "skrapeit-html-parser", version.ref = "skrapeit" }
|
||||||
|
|
||||||
# Markdown
|
# Markdown
|
||||||
markdown = { group = "org.jetbrains", name = "markdown", version.ref = "markdown" }
|
markdown-renderer = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-android", version.ref = "markdown-renderer" }
|
||||||
|
|
||||||
|
# Fading Edges
|
||||||
|
fading-edges = { group = "com.github.GIGAMOLE", name = "ComposeFadingEdges", version.ref = "fading-edges"}
|
||||||
|
|
||||||
# LibSU
|
# LibSU
|
||||||
libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
|
libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
|
||||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,7 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=591855b517fc635b9e04de1d05d5e76ada3f89f5fc76f87978d1b245b4f69225
|
distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
14
gradlew
vendored
14
gradlew
vendored
@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
@ -202,11 +202,11 @@ fi
|
|||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
# Collect all arguments for the java command:
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
# and any embedded shellness will be escaped.
|
||||||
# double quotes to make sure that they get re-expanded; and
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
@ -1,59 +1,19 @@
|
|||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
// TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed
|
|
||||||
val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) {
|
|
||||||
File(".gradle/gradle.properties").inputStream().use {
|
|
||||||
java.util.Properties().apply { load(it) }.let {
|
|
||||||
it.getProperty("gpr.user") to it.getProperty("gpr.key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null to null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun RepositoryHandler.githubPackages(name: String) = maven {
|
|
||||||
url = uri(name)
|
|
||||||
credentials {
|
|
||||||
username = gprUser ?: providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
|
|
||||||
password = gprKey ?: providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://jitpack.io")
|
maven("https://jitpack.io")
|
||||||
githubPackages("https://maven.pkg.github.com/revanced/revanced-patcher")
|
mavenLocal()
|
||||||
githubPackages("https://maven.pkg.github.com/revanced/revanced-library")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
repositories {
|
repositories {
|
||||||
// TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed
|
|
||||||
val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) {
|
|
||||||
File(".gradle/gradle.properties").inputStream().use {
|
|
||||||
java.util.Properties().apply { load(it) }.let {
|
|
||||||
it.getProperty("gpr.user") to it.getProperty("gpr.key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null to null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun RepositoryHandler.githubPackages(name: String) = maven {
|
|
||||||
url = uri(name)
|
|
||||||
credentials {
|
|
||||||
username = gprUser ?: providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
|
|
||||||
password = gprKey ?: providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://jitpack.io")
|
maven("https://jitpack.io")
|
||||||
githubPackages("https://maven.pkg.github.com/revanced/revanced-patcher")
|
mavenLocal()
|
||||||
githubPackages("https://maven.pkg.github.com/revanced/revanced-library")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rootProject.name = "ReVanced Manager"
|
rootProject.name = "ReVanced Manager"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user