mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-28 04:10: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
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up cache
|
||||
uses: actions/cache@v3
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
path: |
|
||||
${{ runner.home }}/.gradle/caches
|
||||
${{ runner.home }}/.gradle/wrapper
|
||||
.gradle
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Set up Java
|
||||
run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
- name: Build with Gradle
|
||||
env:
|
||||
|
12
.github/workflows/release-build.yml
vendored
12
.github/workflows/release-build.yml
vendored
@ -14,8 +14,16 @@ jobs:
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Java
|
||||
run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
|
||||
- name: Set up JDK 17
|
||||
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
|
||||
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 {
|
||||
namespace = "app.revanced.manager"
|
||||
compileSdk = 33
|
||||
buildToolsVersion = "33.0.2"
|
||||
compileSdk = 34
|
||||
buildToolsVersion = "34.0.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.revanced.manager"
|
||||
minSdk = 26
|
||||
targetSdk = 33
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "0.0.1"
|
||||
resourceConfigurations.addAll(listOf(
|
||||
@ -54,6 +54,7 @@ android {
|
||||
includeInApk = false
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes.addAll(listOf(
|
||||
"/prebuilt/**",
|
||||
@ -156,6 +157,9 @@ dependencies {
|
||||
implementation(libs.ktor.content.negotiation)
|
||||
implementation(libs.ktor.serialization)
|
||||
|
||||
// Markdown to HTML
|
||||
implementation(libs.markdown)
|
||||
// Markdown
|
||||
implementation(libs.markdown.renderer)
|
||||
|
||||
// Fading Edges
|
||||
implementation(libs.fading.edges)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "5515d164bc8f713201506d42a02d337f",
|
||||
"identityHash": "802fa2fda94b930bf0ebb85d195f1022",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "patch_bundles",
|
||||
@ -160,7 +160,7 @@
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
@ -175,8 +175,8 @@
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "file",
|
||||
"columnName": "file",
|
||||
"fieldPath": "directory",
|
||||
"columnName": "directory",
|
||||
"affinity": "TEXT",
|
||||
"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": [],
|
||||
"setupQueries": [
|
||||
"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_DELETE_PACKAGES" />
|
||||
<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.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
@ -33,7 +34,7 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.ReVancedManager"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
tools:targetApi="33">
|
||||
tools:targetApi="34">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@ -49,6 +50,17 @@
|
||||
<service android:name=".service.InstallService" />
|
||||
<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
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
|
@ -3,4 +3,4 @@ name=__LABEL__ ReVanced
|
||||
version=__VERSION__
|
||||
versionCode=0
|
||||
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.compose.animation.ExperimentalAnimationApi
|
||||
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.ui.res.stringResource
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||
import app.revanced.manager.ui.destination.Destination
|
||||
import app.revanced.manager.ui.screen.AppInfoScreen
|
||||
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
||||
import app.revanced.manager.ui.destination.SettingsDestination
|
||||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||
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.PatchesSelectorScreen
|
||||
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
||||
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.Theme
|
||||
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.NavBackHandler
|
||||
import dev.olshevski.navigation.reimagined.navigate
|
||||
import dev.olshevski.navigation.reimagined.pop
|
||||
import dev.olshevski.navigation.reimagined.popUpTo
|
||||
import dev.olshevski.navigation.reimagined.rememberNavController
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
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() {
|
||||
@ExperimentalAnimationApi
|
||||
@ -36,7 +45,9 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
installSplashScreen()
|
||||
|
||||
val vm: MainViewModel = getActivityViewModel()
|
||||
val vm: MainViewModel = getAndroidViewModel()
|
||||
|
||||
vm.importLegacySettings(this)
|
||||
|
||||
setContent {
|
||||
val theme by vm.prefs.theme.getAsState()
|
||||
@ -51,9 +62,32 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
NavBackHandler(navController)
|
||||
|
||||
val showAutoUpdatesDialog by vm.prefs.showAutoUpdatesDialog.getAsState()
|
||||
if (showAutoUpdatesDialog) {
|
||||
AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
|
||||
val firstLaunch by vm.prefs.firstLaunch.getAsState()
|
||||
|
||||
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(
|
||||
@ -61,26 +95,44 @@ class MainActivity : ComponentActivity() {
|
||||
) { destination ->
|
||||
when (destination) {
|
||||
is Destination.Dashboard -> DashboardScreen(
|
||||
onSettingsClick = { navController.navigate(Destination.Settings) },
|
||||
onSettingsClick = { navController.navigate(Destination.Settings()) },
|
||||
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 ->
|
||||
navController.navigate(Destination.VersionSelector(packageName, patchesSelection))
|
||||
navController.navigate(
|
||||
Destination.VersionSelector(
|
||||
packageName,
|
||||
patchesSelection
|
||||
)
|
||||
)
|
||||
},
|
||||
onBackClick = { navController.pop() },
|
||||
viewModel = getViewModel { parametersOf(destination.installedApp) }
|
||||
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
|
||||
)
|
||||
|
||||
is Destination.Settings -> SettingsScreen(
|
||||
onBackClick = { navController.pop() }
|
||||
onBackClick = { navController.pop() },
|
||||
startDestination = destination.startDestination
|
||||
)
|
||||
|
||||
is Destination.AppSelector -> AppSelectorScreen(
|
||||
onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
|
||||
onStorageClick = { navController.navigate(Destination.PatchesSelector(it)) },
|
||||
onStorageClick = {
|
||||
navController.navigate(
|
||||
Destination.SelectedApplicationInfo(
|
||||
it
|
||||
)
|
||||
)
|
||||
},
|
||||
onBackClick = { navController.pop() }
|
||||
)
|
||||
|
||||
@ -88,36 +140,46 @@ class MainActivity : ComponentActivity() {
|
||||
onBackClick = { navController.pop() },
|
||||
onAppClick = { selectedApp ->
|
||||
navController.navigate(
|
||||
Destination.PatchesSelector(
|
||||
Destination.SelectedApplicationInfo(
|
||||
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
|
||||
)
|
||||
)
|
||||
},
|
||||
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(
|
||||
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.bundles.PatchBundleDao
|
||||
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
|
||||
|
||||
@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)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun patchBundleDao(): PatchBundleDao
|
||||
abstract fun selectionDao(): SelectionDao
|
||||
abstract fun downloadedAppDao(): DownloadedAppDao
|
||||
abstract fun installedAppDao(): InstalledAppDao
|
||||
abstract fun optionDao(): OptionDao
|
||||
|
||||
companion object {
|
||||
fun generateUid() = Random.Default.nextInt()
|
||||
|
@ -16,5 +16,5 @@ class Converters {
|
||||
fun fileFromString(value: String) = File(value)
|
||||
|
||||
@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(
|
||||
@ColumnInfo(name = "package_name") val packageName: 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.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
@ -24,17 +25,21 @@ interface InstalledAppDao {
|
||||
suspend fun getPatchesSelection(packageName: String): Map<Int, List<String>>
|
||||
|
||||
@Transaction
|
||||
suspend fun insertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) {
|
||||
insertApp(installedApp)
|
||||
suspend fun upsertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) {
|
||||
upsertApp(installedApp)
|
||||
deleteAppliedPatches(installedApp.currentPackageName)
|
||||
insertAppliedPatches(appliedPatches)
|
||||
}
|
||||
|
||||
@Insert
|
||||
suspend fun insertApp(installedApp: InstalledApp)
|
||||
@Upsert
|
||||
suspend fun upsertApp(installedApp: InstalledApp)
|
||||
|
||||
@Insert
|
||||
suspend fun insertAppliedPatches(appliedPatches: List<AppliedPatch>)
|
||||
|
||||
@Query("DELETE FROM applied_patch WHERE package_name = :packageName")
|
||||
suspend fun deleteAppliedPatches(packageName: String)
|
||||
|
||||
@Delete
|
||||
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
|
||||
open suspend fun updateSelections(selections: Map<Int, Set<String>>) =
|
||||
selections.map { (selectionUid, patches) ->
|
||||
selections.forEach { (selectionUid, patches) ->
|
||||
clearSelection(selectionUid)
|
||||
selectPatches(patches.map { SelectedPatch(selectionUid, it) })
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.manager.BuildConfig
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.UserAgent
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -40,6 +42,9 @@ val httpModule = module {
|
||||
install(HttpTimeout) {
|
||||
socketTimeoutMillis = 10000
|
||||
}
|
||||
install(UserAgent) {
|
||||
agent = "ReVanced-Manager/${BuildConfig.VERSION_CODE}"
|
||||
}
|
||||
}
|
||||
|
||||
fun provideJson() = Json {
|
||||
|
@ -17,7 +17,11 @@ val repositoryModule = module {
|
||||
singleOf(::NetworkInfo)
|
||||
singleOf(::PatchBundlePersistenceRepository)
|
||||
singleOf(::PatchSelectionRepository)
|
||||
singleOf(::PatchBundleRepository)
|
||||
singleOf(::PatchOptionsRepository)
|
||||
singleOf(::PatchBundleRepository) {
|
||||
// It is best to load patch bundles ASAP
|
||||
createdAtStart()
|
||||
}
|
||||
singleOf(::WorkerRepository)
|
||||
singleOf(::DownloadedAppRepository)
|
||||
singleOf(::InstalledAppRepository)
|
||||
|
@ -7,17 +7,18 @@ import org.koin.dsl.module
|
||||
val viewModelModule = module {
|
||||
viewModelOf(::MainViewModel)
|
||||
viewModelOf(::DashboardViewModel)
|
||||
viewModelOf(::SelectedAppInfoViewModel)
|
||||
viewModelOf(::PatchesSelectorViewModel)
|
||||
viewModelOf(::SettingsViewModel)
|
||||
viewModelOf(::AdvancedSettingsViewModel)
|
||||
viewModelOf(::AppSelectorViewModel)
|
||||
viewModelOf(::VersionSelectorViewModel)
|
||||
viewModelOf(::InstallerViewModel)
|
||||
viewModelOf(::UpdateProgressViewModel)
|
||||
viewModelOf(::ManagerUpdateChangelogViewModel)
|
||||
viewModelOf(::UpdateViewModel)
|
||||
viewModelOf(::ChangelogsViewModel)
|
||||
viewModelOf(::ImportExportViewModel)
|
||||
viewModelOf(::ContributorViewModel)
|
||||
viewModelOf(::DownloadsViewModel)
|
||||
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) {
|
||||
suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
patches?.let {
|
||||
Files.copy(it, patchesFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
patches?.let { inputStream ->
|
||||
patchBundleOutputStream().use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
integrations?.let {
|
||||
Files.copy(it, this@LocalPatchBundle.integrationsFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
|
@ -1,11 +1,14 @@
|
||||
package app.revanced.manager.domain.bundles
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.util.tag
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* A [PatchBundle] source.
|
||||
@ -23,12 +26,23 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
|
||||
*/
|
||||
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 {
|
||||
if (!hasInstalled()) return State.Missing
|
||||
|
||||
return try {
|
||||
State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
|
||||
} catch (t: Throwable) {
|
||||
Log.e(tag, "Failed to load patch bundle $name", t)
|
||||
State.Failed(t)
|
||||
}
|
||||
}
|
||||
@ -40,7 +54,7 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
|
||||
sealed interface State {
|
||||
fun patchBundleOrNull(): PatchBundle? = null
|
||||
|
||||
object Missing : State
|
||||
data object Missing : State
|
||||
data class Failed(val throwable: Throwable) : State
|
||||
data class Loaded(val bundle: PatchBundle) : State {
|
||||
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) {
|
||||
val (patches, integrations) = info
|
||||
coroutineScope {
|
||||
mapOf(
|
||||
patches.url to patchesFile,
|
||||
integrations.url to integrationsFile
|
||||
).forEach { (asset, file) ->
|
||||
launch {
|
||||
http.download(file) {
|
||||
url(asset)
|
||||
launch {
|
||||
patchBundleOutputStream().use {
|
||||
http.streamTo(it) {
|
||||
url(patches.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
http.download(integrationsFile) {
|
||||
url(integrations.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveVersion(patches.version, integrations.version)
|
||||
@ -101,7 +104,7 @@ class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
||||
override suspend fun getLatestInfo() = coroutineScope {
|
||||
fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) {
|
||||
api
|
||||
.getRelease(repo)
|
||||
.getLatestRelease(repo)
|
||||
.getOrThrow()
|
||||
.let {
|
||||
BundleAsset(it.metadata.tag, it.findAssetByType(mime).downloadUrl)
|
||||
|
@ -12,6 +12,7 @@ class PreferencesManager(
|
||||
|
||||
val api = stringPreference("api_url", "https://api.revanced.app")
|
||||
|
||||
val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true)
|
||||
val allowExperimental = booleanPreference("allow_experimental", false)
|
||||
|
||||
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
||||
@ -19,7 +20,7 @@ class PreferencesManager(
|
||||
|
||||
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 disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
||||
|
@ -1,34 +1,61 @@
|
||||
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.Companion.generateUid
|
||||
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
||||
import app.revanced.manager.network.downloader.AppDownloader
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import java.io.File
|
||||
|
||||
class DownloadedAppRepository(
|
||||
app: Application,
|
||||
db: AppDatabase
|
||||
) {
|
||||
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
|
||||
private val dao = db.downloadedAppDao()
|
||||
|
||||
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(
|
||||
packageName: String,
|
||||
version: String,
|
||||
file: File
|
||||
) = dao.insert(
|
||||
DownloadedApp(
|
||||
packageName = packageName,
|
||||
version = version,
|
||||
file = file
|
||||
)
|
||||
)
|
||||
suspend fun download(
|
||||
app: AppDownloader.App,
|
||||
preferSplits: Boolean,
|
||||
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit = {},
|
||||
): File {
|
||||
this.get(app.packageName, app.version)?.let { downloaded ->
|
||||
return getApkFileForApp(downloaded)
|
||||
}
|
||||
|
||||
// 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>) {
|
||||
downloadedApps.forEach {
|
||||
it.file.deleteRecursively()
|
||||
dir.resolve(it.directory).deleteRecursively()
|
||||
}
|
||||
|
||||
dao.delete(downloadedApps)
|
||||
|
@ -19,14 +19,14 @@ class InstalledAppRepository(
|
||||
suspend fun getAppliedPatches(packageName: String): PatchesSelection =
|
||||
dao.getPatchesSelection(packageName).mapValues { (_, patches) -> patches.toSet() }
|
||||
|
||||
suspend fun add(
|
||||
suspend fun addOrUpdate(
|
||||
currentPackageName: String,
|
||||
originalPackageName: String,
|
||||
version: String,
|
||||
installType: InstallType,
|
||||
patchesSelection: PatchesSelection
|
||||
) {
|
||||
dao.insertApp(
|
||||
dao.upsertApp(
|
||||
InstalledApp(
|
||||
currentPackageName = currentPackageName,
|
||||
originalPackageName = originalPackageName,
|
||||
|
@ -3,6 +3,7 @@ package app.revanced.manager.domain.repository
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||
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.util.flatMapLatestAndCombine
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.InputStream
|
||||
|
||||
class PatchBundleRepository(
|
||||
app: Application,
|
||||
private val app: Application,
|
||||
private val persistenceRepo: PatchBundlePersistenceRepository,
|
||||
private val networkInfo: NetworkInfo,
|
||||
) {
|
||||
@ -124,20 +126,24 @@ class PatchBundleRepository(
|
||||
reload()
|
||||
}
|
||||
|
||||
suspend fun redownloadRemoteBundles() = getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() }
|
||||
suspend fun redownloadRemoteBundles() =
|
||||
getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() }
|
||||
|
||||
suspend fun updateCheck() = supervisorScope {
|
||||
if (!networkInfo.isSafe()) {
|
||||
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||
return@supervisorScope
|
||||
}
|
||||
suspend fun updateCheck() =
|
||||
uiSafe(app, R.string.source_download_fail, "Failed to update bundles") {
|
||||
coroutineScope {
|
||||
if (!networkInfo.isSafe()) {
|
||||
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||
return@coroutineScope
|
||||
}
|
||||
|
||||
getBundlesByType<RemotePatchBundle>().forEach {
|
||||
launch {
|
||||
if (!it.propsFlow().first().autoUpdate) return@launch
|
||||
Log.d(tag, "Updating patch bundle: ${it.name}")
|
||||
it.update()
|
||||
getBundlesByType<RemotePatchBundle>().forEach {
|
||||
launch {
|
||||
if (!it.propsFlow().first().autoUpdate) return@launch
|
||||
Log.d(tag, "Updating patch bundle: ${it.name}")
|
||||
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
|
||||
|
||||
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.service.ReVancedService
|
||||
import app.revanced.manager.network.utils.getOrThrow
|
||||
import app.revanced.manager.network.utils.transform
|
||||
|
||||
class ReVancedAPI(
|
||||
@ -16,7 +13,9 @@ class ReVancedAPI(
|
||||
|
||||
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 {
|
||||
fun ReVancedRelease.findAssetByType(mime: String) = assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime)
|
||||
|
@ -171,7 +171,7 @@ class APKMirror : AppDownloader, KoinComponent {
|
||||
saveDirectory: File,
|
||||
preferSplit: Boolean,
|
||||
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit
|
||||
): File {
|
||||
) {
|
||||
val variants = httpClient.getHtml { url(apkMirror + downloadLink) }
|
||||
.div {
|
||||
withClass = "variants-table"
|
||||
@ -246,18 +246,10 @@ class APKMirror : AppDownloader, KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
val saveLocation = if (variant.apkType == APKType.BUNDLE)
|
||||
saveDirectory.resolve(version).also { it.mkdirs() }
|
||||
else
|
||||
saveDirectory.resolve("$version.apk")
|
||||
val targetFile = saveDirectory.resolve("base.apk")
|
||||
|
||||
try {
|
||||
val downloadLocation = if (variant.apkType == APKType.BUNDLE)
|
||||
saveLocation.resolve("temp.zip")
|
||||
else
|
||||
saveLocation
|
||||
|
||||
httpClient.download(downloadLocation) {
|
||||
httpClient.download(targetFile) {
|
||||
url(apkMirror + downloadLink)
|
||||
onDownload { bytesSentTotal, contentLength ->
|
||||
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) {
|
||||
// TODO: Extract temp.zip
|
||||
|
||||
downloadLocation.delete()
|
||||
targetFile.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
saveLocation.deleteRecursively()
|
||||
throw e
|
||||
} finally {
|
||||
onDownload(null)
|
||||
}
|
||||
|
||||
return saveLocation
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,6 @@ interface AppDownloader {
|
||||
saveDirectory: File,
|
||||
preferSplit: Boolean,
|
||||
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit = {}
|
||||
): File
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -8,6 +8,11 @@ data class ReVancedLatestRelease(
|
||||
val release: ReVancedRelease,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedReleases(
|
||||
val releases: List<ReVancedRelease>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedRelease(
|
||||
val metadata: ReVancedReleaseMeta,
|
||||
@ -28,6 +33,7 @@ data class ReVancedReleaseMeta(
|
||||
@Serializable
|
||||
data class Asset(
|
||||
val name: String,
|
||||
@SerialName("download_count") val downloadCount: Int,
|
||||
@SerialName("browser_download_url") val downloadUrl: 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.readBytes
|
||||
import it.skrape.core.htmlDocument
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* @author Aliucord Authors, DiamondMiner88
|
||||
@ -49,7 +52,10 @@ class HttpService(
|
||||
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))
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
@ -59,20 +65,19 @@ class HttpService(
|
||||
return response
|
||||
}
|
||||
|
||||
suspend fun download(
|
||||
saveLocation: File,
|
||||
suspend fun streamTo(
|
||||
outputStream: OutputStream,
|
||||
builder: HttpRequestBuilder.() -> Unit
|
||||
) {
|
||||
http.prepareGet(builder).execute { httpResponse ->
|
||||
if (httpResponse.status.isSuccess()) {
|
||||
|
||||
saveLocation.outputStream().use { stream ->
|
||||
val channel: ByteReadChannel = httpResponse.body()
|
||||
val channel: ByteReadChannel = httpResponse.body()
|
||||
withContext(Dispatchers.IO) {
|
||||
while (!channel.isClosedForRead) {
|
||||
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||
while (packet.isNotEmpty) {
|
||||
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(
|
||||
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.ReVancedGitRepositories
|
||||
import app.revanced.manager.network.dto.ReVancedReleases
|
||||
import app.revanced.manager.network.utils.APIResponse
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -10,13 +11,20 @@ import kotlinx.coroutines.withContext
|
||||
class ReVancedService(
|
||||
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) {
|
||||
client.request {
|
||||
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> =
|
||||
withContext(Dispatchers.IO) {
|
||||
client.request {
|
||||
|
@ -20,6 +20,7 @@ class Session(
|
||||
cacheDir: String,
|
||||
frameworkDir: String,
|
||||
aaptPath: String,
|
||||
multithreadingDexFileWriter: Boolean,
|
||||
private val logger: ManagerLogger,
|
||||
private val input: File,
|
||||
private val onStepSucceeded: suspend () -> Unit
|
||||
@ -30,7 +31,8 @@ class Session(
|
||||
inputFile = input,
|
||||
resourceCachePath = tempDir.resolve("aapt-resources"),
|
||||
frameworkFileDirectory = frameworkDir,
|
||||
aaptBinaryPath = aaptPath
|
||||
aaptBinaryPath = aaptPath,
|
||||
multithreadingDexFileWriter = multithreadingDexFileWriter,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -1,9 +1,14 @@
|
||||
package app.revanced.manager.patcher.aapt
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build.SUPPORTED_ABIS as DEVICE_ABIS
|
||||
import java.io.File
|
||||
|
||||
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? {
|
||||
return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
|
||||
}
|
||||
|
@ -9,7 +9,10 @@ import java.io.File
|
||||
class PatchBundle(private val loader: Iterable<Patch<*>>, val integrations: File?) {
|
||||
constructor(bundleJar: File, integrations: File?) : this(
|
||||
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()
|
||||
},
|
||||
|
@ -56,15 +56,15 @@ data class Option(
|
||||
val key: String,
|
||||
val description: String,
|
||||
val required: Boolean,
|
||||
val type: Class<out PatchOption<*>>,
|
||||
val defaultValue: Any?
|
||||
val type: String,
|
||||
val default: Any?
|
||||
) {
|
||||
constructor(option: PatchOption<*>) : this(
|
||||
option.title ?: option.key,
|
||||
option.key,
|
||||
option.description.orEmpty(),
|
||||
option.required,
|
||||
option::class.java,
|
||||
option.value
|
||||
option.valueType,
|
||||
option.default,
|
||||
)
|
||||
}
|
@ -26,7 +26,12 @@ class Step(
|
||||
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)
|
||||
private var currentStep: StepKey? = StepKey(0, 0)
|
||||
|
||||
@ -87,12 +92,20 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>, se
|
||||
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(
|
||||
R.string.patcher_step_group_prepare,
|
||||
listOfNotNull(
|
||||
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_integrations))
|
||||
).toImmutableList()
|
||||
@ -100,7 +113,10 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>, se
|
||||
generatePatchesStep(selectedPatches),
|
||||
Step(
|
||||
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.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
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.room.apps.installed.InstallType
|
||||
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.repository.DownloadedAppRepository
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
@ -48,6 +51,7 @@ class PatcherWorker(
|
||||
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||
private val workerRepository: WorkerRepository by inject()
|
||||
private val prefs: PreferencesManager by inject()
|
||||
private val keystoreManager: KeystoreManager by inject()
|
||||
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
||||
private val pm: PM by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
@ -71,7 +75,12 @@ class PatcherWorker(
|
||||
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 {
|
||||
val notificationIntent = Intent(applicationContext, PatcherWorker::class.java)
|
||||
@ -152,6 +161,8 @@ class PatcherWorker(
|
||||
progressFlow.value = progressManager.getProgress().toImmutableList()
|
||||
}
|
||||
|
||||
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||
|
||||
return try {
|
||||
|
||||
if (args.input is SelectedApp.Installed) {
|
||||
@ -168,11 +179,11 @@ class PatcherWorker(
|
||||
.mapValues { (_, bundle) -> bundle.patchClasses(args.packageName) }
|
||||
|
||||
// Set all patch options.
|
||||
args.options.forEach { (bundle, configuredPatchOptions) ->
|
||||
args.options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
configuredPatchOptions.forEach { (patchName, options) ->
|
||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||
val patchOptions = patches.single { it.name == patchName }.options
|
||||
options.forEach { (key, value) ->
|
||||
configuredPatchOptions.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
}
|
||||
@ -190,19 +201,11 @@ class PatcherWorker(
|
||||
|
||||
val inputFile = when (val selectedApp = args.input) {
|
||||
is SelectedApp.Download -> {
|
||||
val savePath = applicationContext.filesDir.resolve("downloaded-apps")
|
||||
.resolve(args.input.packageName).also { it.mkdirs() }
|
||||
|
||||
selectedApp.app.download(
|
||||
savePath,
|
||||
downloadedAppRepository.download(
|
||||
selectedApp.app,
|
||||
prefs.preferSplits.get(),
|
||||
onDownload = { downloadProgress.emit(it) }
|
||||
).also {
|
||||
downloadedAppRepository.add(
|
||||
args.input.packageName,
|
||||
args.input.version,
|
||||
it
|
||||
)
|
||||
args.setInputFile(it)
|
||||
updateProgress() // Downloading
|
||||
}
|
||||
@ -216,13 +219,17 @@ class PatcherWorker(
|
||||
fs.tempDir.absolutePath,
|
||||
frameworkPath,
|
||||
aaptPath,
|
||||
prefs.multithreadingDexFileWriter.get(),
|
||||
args.logger,
|
||||
inputFile,
|
||||
onStepSucceeded = ::updateProgress
|
||||
).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())
|
||||
progressManager.success()
|
||||
Result.success()
|
||||
@ -232,6 +239,7 @@ class PatcherWorker(
|
||||
Result.failure()
|
||||
} finally {
|
||||
updateProgress(false)
|
||||
patchedApk.delete()
|
||||
}
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ class InstallService : Service() {
|
||||
else -> {
|
||||
sendBroadcast(Intent().apply {
|
||||
action = APP_INSTALL_ACTION
|
||||
`package` = packageName
|
||||
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
|
||||
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
|
||||
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
|
||||
|
@ -31,7 +31,7 @@ class UninstallService : Service() {
|
||||
else -> {
|
||||
sendBroadcast(Intent().apply {
|
||||
action = APP_UNINSTALL_ACTION
|
||||
|
||||
`package` = packageName
|
||||
putExtra(EXTRA_UNINSTALL_STATUS, extraStatus)
|
||||
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,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
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
|
||||
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import app.revanced.manager.util.hexCode
|
||||
import app.revanced.manager.util.openUrl
|
||||
import com.google.accompanist.web.AccompanistWebViewClient
|
||||
import com.google.accompanist.web.WebView
|
||||
import com.google.accompanist.web.rememberWebViewStateWithHTMLData
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mikepenz.markdown.compose.Markdown
|
||||
import com.mikepenz.markdown.model.markdownColor
|
||||
import com.mikepenz.markdown.model.markdownTypography
|
||||
|
||||
@Composable
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun Markdown(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
text: String
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
val markdown = text.trimIndent()
|
||||
|
||||
WebView(
|
||||
state,
|
||||
modifier = Modifier
|
||||
.background(Color.Transparent)
|
||||
.then(modifier),
|
||||
client = client,
|
||||
captureBackPresses = false,
|
||||
onCreated = {
|
||||
it.setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||
it.isVerticalScrollBarEnabled = false
|
||||
it.isHorizontalScrollBarEnabled = false
|
||||
it.setOnTouchListener { _, event -> event.action == MotionEvent.ACTION_MOVE }
|
||||
it.layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
Markdown(
|
||||
content = markdown,
|
||||
colors = markdownColor(
|
||||
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
codeBackground = MaterialTheme.colorScheme.secondaryContainer,
|
||||
codeText = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
),
|
||||
typography = markdownTypography(
|
||||
h1 = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),
|
||||
h2 = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
|
||||
h3 = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||
text = MaterialTheme.typography.bodyMedium,
|
||||
list = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.width
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
|
||||
@Composable
|
||||
fun NotificationCard(
|
||||
color: Color,
|
||||
icon: ImageVector,
|
||||
isWarning: Boolean = false,
|
||||
title: String? = null,
|
||||
text: String,
|
||||
content: @Composable () -> Unit
|
||||
icon: ImageVector,
|
||||
actions: (@Composable () -> Unit)?
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(28.dp))
|
||||
.background(color)
|
||||
) {
|
||||
val color =
|
||||
if (isWarning) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onPrimaryContainer
|
||||
|
||||
NotificationCardInstance(isWarning = isWarning) {
|
||||
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(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
16.dp,
|
||||
Alignment.Start
|
||||
)
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(if (title != null) 36.dp else 24.dp),
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.width(220.dp),
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
if (title != null) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,12 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
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.TextInputDialog
|
||||
import app.revanced.manager.util.isDebuggable
|
||||
|
||||
@Composable
|
||||
fun BaseBundleDialog(
|
||||
@ -159,20 +161,18 @@ fun BaseBundleDialog(
|
||||
)
|
||||
}
|
||||
|
||||
val patchesClickable = LocalContext.current.isDebuggable && patchCount > 0
|
||||
BundleListItem(
|
||||
headlineText = stringResource(R.string.patches),
|
||||
supportingText = if (patchCount == 0) stringResource(R.string.no_patches)
|
||||
else stringResource(R.string.patches_available, patchCount),
|
||||
modifier = Modifier.clickable(enabled = patchCount > 0) {
|
||||
onPatchesClick()
|
||||
}
|
||||
modifier = Modifier.clickable(enabled = patchesClickable, onClick = onPatchesClick)
|
||||
) {
|
||||
if (patchCount > 0) {
|
||||
if (patchesClickable)
|
||||
Icon(
|
||||
Icons.Outlined.ArrowRight,
|
||||
stringResource(R.string.patches)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
version?.let {
|
||||
|
@ -70,17 +70,10 @@ fun BundlePatchesDialog(
|
||||
item {
|
||||
AnimatedVisibility(visible = informationCardVisible) {
|
||||
NotificationCard(
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
icon = Icons.Outlined.Lightbulb,
|
||||
text = stringResource(R.string.tap_on_patches)
|
||||
) {
|
||||
IconButton(onClick = { informationCardVisible = false }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.close),
|
||||
)
|
||||
}
|
||||
}
|
||||
text = stringResource(R.string.tap_on_patches),
|
||||
onDismiss = { informationCardVisible = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,6 @@ import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.patcher.patch.Option
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.patcher.patch.options.types.*
|
||||
import org.koin.compose.rememberKoinInject
|
||||
|
||||
// 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 ->
|
||||
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, _, _ ->
|
||||
private val unknownOption: OptionImpl = { option, _, _ ->
|
||||
val context = LocalContext.current
|
||||
OptionListItem(
|
||||
option = option,
|
||||
onClick = { context.toast("Unknown type: ${option.type.name}") },
|
||||
onClick = { context.toast("Unknown type: ${option.type}") },
|
||||
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
|
||||
fun OptionItem(option: Option, value: Any?, setValue: (Any?) -> Unit) {
|
||||
val implementation = remember(option.type) {
|
||||
when (option.type) {
|
||||
// These are the only two types that are currently used by the official patches.
|
||||
StringPatchOption::class.java -> StringOption
|
||||
BooleanPatchOption::class.java -> BooleanOption
|
||||
else -> UnknownOption
|
||||
}
|
||||
optionImplementations.getOrDefault(
|
||||
option.type,
|
||||
unknownOption
|
||||
)
|
||||
}
|
||||
|
||||
implementation(option, value, setValue)
|
||||
|
@ -2,9 +2,7 @@ package app.revanced.manager.ui.component.settings
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@ -37,10 +35,10 @@ fun BooleanItem(
|
||||
onValueChange: (Boolean) -> Unit,
|
||||
@StringRes headline: Int,
|
||||
@StringRes description: Int
|
||||
) = ListItem(
|
||||
) = SettingsListItem(
|
||||
modifier = Modifier.clickable { onValueChange(!value) },
|
||||
headlineContent = { Text(stringResource(headline)) },
|
||||
supportingContent = { Text(stringResource(description)) },
|
||||
headlineContent = stringResource(headline),
|
||||
supportingContent = stringResource(description),
|
||||
trailingContent = {
|
||||
Switch(
|
||||
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 {
|
||||
|
||||
@Parcelize
|
||||
object Dashboard : Destination
|
||||
data object Dashboard : Destination
|
||||
|
||||
@Parcelize
|
||||
data class ApplicationInfo(val installedApp: InstalledApp) : Destination
|
||||
data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination
|
||||
|
||||
@Parcelize
|
||||
object AppSelector : Destination
|
||||
data object AppSelector : Destination
|
||||
|
||||
@Parcelize
|
||||
object Settings : Destination
|
||||
data class Settings(val startDestination: SettingsDestination = SettingsDestination.Settings) : Destination
|
||||
|
||||
@Parcelize
|
||||
data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination
|
||||
|
||||
@Parcelize
|
||||
data class PatchesSelector(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
|
||||
data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
|
||||
|
||||
@Parcelize
|
||||
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
|
||||
|
||||
@Parcelize
|
||||
object UpdateProgress : SettingsDestination
|
||||
data class Update(val downloadOnScreenEntry: Boolean) : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
object UpdateChangelog : SettingsDestination
|
||||
object Changelogs : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
object Contributors: SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
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
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@ -65,7 +66,12 @@ fun DashboardScreen(
|
||||
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
||||
val androidContext = LocalContext.current
|
||||
|
||||
val pagerState = rememberPagerState()
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = DashboardPage.DASHBOARD.ordinal,
|
||||
initialPageOffsetFraction = 0f
|
||||
) {
|
||||
DashboardPage.values().size
|
||||
}
|
||||
val composableScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
@ -186,7 +192,6 @@ fun DashboardScreen(
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
pageCount = pages.size,
|
||||
state = pagerState,
|
||||
userScrollEnabled = true,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@ -199,6 +204,13 @@ fun DashboardScreen(
|
||||
}
|
||||
|
||||
DashboardPage.BUNDLES -> {
|
||||
BackHandler {
|
||||
if (bundlesSelectable) vm.cancelSourceSelection() else composableScope.launch {
|
||||
pagerState.animateScrollToPage(
|
||||
DashboardPage.DASHBOARD.ordinal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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.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.shape.RoundedCornerShape
|
||||
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.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
|
||||
@ -32,7 +29,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
@ -40,19 +36,19 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.ui.component.AppIcon
|
||||
import app.revanced.manager.ui.component.AppLabel
|
||||
import app.revanced.manager.ui.component.AppInfo
|
||||
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.viewmodel.AppInfoViewModel
|
||||
import app.revanced.manager.ui.viewmodel.InstalledAppInfoViewModel
|
||||
import app.revanced.manager.util.PatchesSelection
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppInfoScreen(
|
||||
fun InstalledAppInfoScreen(
|
||||
onPatchClick: (packageName: String, patchesSelection: PatchesSelection) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: AppInfoViewModel
|
||||
viewModel: InstalledAppInfoViewModel
|
||||
) {
|
||||
SideEffect {
|
||||
viewModel.onBackClick = onBackClick
|
||||
@ -80,27 +76,8 @@ fun AppInfoScreen(
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.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)
|
||||
AppInfo(viewModel.appInfo) {
|
||||
Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
|
||||
|
||||
if (viewModel.installedApp.installType == InstallType.ROOT) {
|
||||
Text(
|
||||
@ -166,38 +143,35 @@ fun AppInfoScreen(
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
) {
|
||||
ListItem(
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { },
|
||||
headlineContent = { Text(stringResource(R.string.applied_patches)) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
headlineContent = stringResource(R.string.applied_patches),
|
||||
supportingContent =
|
||||
(viewModel.appliedPatches?.values?.sumOf { it.size } ?: 0).let {
|
||||
pluralStringResource(
|
||||
id = R.plurals.applied_patches,
|
||||
it,
|
||||
it
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
trailingContent = { Icon(Icons.Filled.ArrowRight, contentDescription = stringResource(R.string.view_applied_patches)) }
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.package_name)) },
|
||||
supportingContent = { Text(viewModel.installedApp.currentPackageName) }
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.package_name),
|
||||
supportingContent = viewModel.installedApp.currentPackageName
|
||||
)
|
||||
|
||||
if (viewModel.installedApp.originalPackageName != viewModel.installedApp.currentPackageName) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.original_package_name)) },
|
||||
supportingContent = { Text(viewModel.installedApp.originalPackageName) }
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.original_package_name),
|
||||
supportingContent = viewModel.installedApp.originalPackageName
|
||||
)
|
||||
}
|
||||
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.install_type)) },
|
||||
supportingContent = { Text(stringResource(viewModel.installedApp.installType.stringResource)) }
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.install_type),
|
||||
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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@ -18,9 +21,11 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
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.AppLabel
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.component.NotificationCard
|
||||
import app.revanced.manager.ui.viewmodel.InstalledAppsViewModel
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@ -31,43 +36,55 @@ fun InstalledAppsScreen(
|
||||
) {
|
||||
val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = installedApps?.let { if (it.isEmpty()) Arrangement.Center else Arrangement.Top } ?: Arrangement.Center
|
||||
) {
|
||||
installedApps?.let { installedApps ->
|
||||
Column {
|
||||
if (!Aapt.supportsDevice()) {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Outlined.WarningAmber,
|
||||
text = stringResource(
|
||||
R.string.unsupported_architecture_warning
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
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) }
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = if (installedApps.isNullOrEmpty()) Arrangement.Center else Arrangement.Top
|
||||
) {
|
||||
installedApps?.let { installedApps ->
|
||||
|
||||
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.filled.Cancel
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@ -36,8 +38,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
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.Step
|
||||
import app.revanced.manager.ui.component.AppScaffold
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ArrowButton
|
||||
@ -59,7 +61,6 @@ fun InstallerScreen(
|
||||
val patcherState by vm.patcherState.observeAsState(null)
|
||||
val steps by vm.progress.collectAsStateWithLifecycle()
|
||||
val canInstall by remember { derivedStateOf { patcherState == true && (vm.installedPackageName != null || !vm.isInstalling) } }
|
||||
var dropdownActive by rememberSaveable { mutableStateOf(false) }
|
||||
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showInstallPicker)
|
||||
@ -72,23 +73,40 @@ fun InstallerScreen(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.installer),
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
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 ->
|
||||
Column(
|
||||
@ -100,33 +118,6 @@ fun InstallerScreen(
|
||||
steps.forEach {
|
||||
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.rememberPagerState
|
||||
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.MoreVert
|
||||
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.Settings
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FilterChip
|
||||
@ -32,16 +31,20 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.SearchBar
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
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.patches.OptionItem
|
||||
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_UNIVERSAL
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
||||
@ -73,18 +75,75 @@ import org.koin.compose.rememberKoinInject
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun PatchesSelectorScreen(
|
||||
onPatchClick: (PatchesSelection, Options) -> Unit,
|
||||
onSave: (PatchesSelection?, Options) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
vm: PatchesSelectorViewModel
|
||||
) {
|
||||
val pagerState = rememberPagerState()
|
||||
val composableScope = rememberCoroutineScope()
|
||||
|
||||
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())
|
||||
UnsupportedDialog(
|
||||
appVersion = vm.input.selectedApp.version,
|
||||
appVersion = vm.appVersion,
|
||||
supportedVersions = vm.compatibleVersions,
|
||||
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(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
@ -115,50 +280,28 @@ fun PatchesSelectorScreen(
|
||||
IconButton(onClick = vm::reset) {
|
||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||
}
|
||||
|
||||
var dropdownActive by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
IconButton(onClick = { showBottomSheet = true }) {
|
||||
Icon(Icons.Outlined.FilterList, stringResource(R.string.more))
|
||||
}
|
||||
// This part should probably be changed
|
||||
IconButton(onClick = { dropdownActive = true }) {
|
||||
Icon(Icons.Outlined.MoreVert, stringResource(R.string.more))
|
||||
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 = {
|
||||
search = ""
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { }) {
|
||||
) {
|
||||
Icon(Icons.Outlined.Search, stringResource(R.string.search))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (!showPatchButton) return@Scaffold
|
||||
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.patch)) },
|
||||
icon = { Icon(Icons.Default.Build, null) },
|
||||
text = { Text(stringResource(R.string.save)) },
|
||||
icon = { Icon(Icons.Outlined.Save, null) },
|
||||
onClick = {
|
||||
// TODO: only allow this if all required options have been set.
|
||||
composableScope.launch {
|
||||
val selection = vm.getSelection()
|
||||
vm.saveSelection(selection).join()
|
||||
onPatchClick(selection, vm.getOptions())
|
||||
}
|
||||
onSave(vm.getCustomSelection(), vm.getOptions())
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -192,113 +335,40 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
pageCount = bundles.size,
|
||||
state = pagerState,
|
||||
userScrollEnabled = true,
|
||||
pageContent = { index ->
|
||||
val bundle = bundles[index]
|
||||
|
||||
Column {
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp, vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.supported,
|
||||
filterFlag = SHOW_SUPPORTED,
|
||||
supported = true
|
||||
)
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.universal,
|
||||
filterFlag = SHOW_UNIVERSAL,
|
||||
supported = true
|
||||
) {
|
||||
FilterChip(
|
||||
selected = vm.filter and SHOW_SUPPORTED != 0 && bundle.supported.isNotEmpty(),
|
||||
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()
|
||||
ListHeader(
|
||||
title = stringResource(R.string.universal_patches),
|
||||
)
|
||||
}
|
||||
|
||||
val allowExperimental by vm.allowExperimental.getAsState()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.unsupported,
|
||||
filterFlag = SHOW_UNSUPPORTED,
|
||||
supported = vm.allowExperimental
|
||||
) {
|
||||
fun LazyListScope.patchList(
|
||||
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 = 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
|
||||
ListHeader(
|
||||
title = stringResource(R.string.unsupported_patches),
|
||||
onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) }
|
||||
)
|
||||
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 = {
|
||||
Checkbox(
|
||||
checked = selected,
|
||||
onCheckedChange = null,
|
||||
onCheckedChange = { onToggle() },
|
||||
enabled = supported
|
||||
)
|
||||
},
|
||||
@ -501,7 +571,7 @@ fun OptionsDialog(
|
||||
items(patch.options, key = { it.key }) { option ->
|
||||
val key = option.key
|
||||
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) })
|
||||
}
|
||||
|
@ -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.provider.Settings
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.NotificationCard
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.destination.SettingsDestination
|
||||
import app.revanced.manager.ui.screen.settings.*
|
||||
import app.revanced.manager.ui.screen.settings.update.ManagerUpdateChangelog
|
||||
import app.revanced.manager.ui.screen.settings.update.UpdateProgressScreen
|
||||
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
|
||||
import app.revanced.manager.ui.screen.settings.update.UpdateScreen
|
||||
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
|
||||
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
||||
import dev.olshevski.navigation.reimagined.*
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.androidx.compose.getViewModel as getComposeViewModel
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
startDestination: SettingsDestination,
|
||||
viewModel: SettingsViewModel = getViewModel()
|
||||
) {
|
||||
val navController =
|
||||
rememberNavController<SettingsDestination>(startDestination = SettingsDestination.Settings)
|
||||
val navController = rememberNavController(startDestination)
|
||||
|
||||
val backClick: () -> Unit = {
|
||||
if (navController.backstack.entries.size == 1)
|
||||
onBackClick()
|
||||
else navController.pop()
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
@ -98,50 +98,54 @@ fun SettingsScreen(
|
||||
controller = navController
|
||||
) { destination ->
|
||||
when (destination) {
|
||||
|
||||
is SettingsDestination.General -> GeneralSettingsScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
onBackClick = backClick,
|
||||
viewModel = viewModel
|
||||
)
|
||||
|
||||
is SettingsDestination.Advanced -> AdvancedSettingsScreen(
|
||||
onBackClick = { navController.pop() }
|
||||
onBackClick = backClick
|
||||
)
|
||||
|
||||
is SettingsDestination.Updates -> UpdatesSettingsScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
onChangelogClick = { navController.navigate(SettingsDestination.UpdateChangelog) },
|
||||
onUpdateClick = { navController.navigate(SettingsDestination.UpdateProgress) }
|
||||
onBackClick = backClick,
|
||||
onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) },
|
||||
onUpdateClick = { navController.navigate(SettingsDestination.Update(false)) }
|
||||
)
|
||||
|
||||
is SettingsDestination.Downloads -> DownloadsSettingsScreen(
|
||||
onBackClick = { navController.pop() }
|
||||
onBackClick = backClick
|
||||
)
|
||||
|
||||
is SettingsDestination.ImportExport -> ImportExportSettingsScreen(
|
||||
onBackClick = { navController.pop() }
|
||||
onBackClick = backClick
|
||||
)
|
||||
|
||||
is SettingsDestination.About -> AboutSettingsScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
onBackClick = backClick,
|
||||
onContributorsClick = { navController.navigate(SettingsDestination.Contributors) },
|
||||
onLicensesClick = { navController.navigate(SettingsDestination.Licenses) }
|
||||
)
|
||||
|
||||
is SettingsDestination.UpdateProgress -> UpdateProgressScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
is SettingsDestination.Update -> UpdateScreen(
|
||||
onBackClick = backClick,
|
||||
vm = getComposeViewModel {
|
||||
parametersOf(
|
||||
destination.downloadOnScreenEntry
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
is SettingsDestination.UpdateChangelog -> ManagerUpdateChangelog(
|
||||
onBackClick = { navController.pop() },
|
||||
is SettingsDestination.Changelogs -> ChangelogsScreen(
|
||||
onBackClick = backClick,
|
||||
)
|
||||
|
||||
is SettingsDestination.Contributors -> ContributorScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
onBackClick = backClick,
|
||||
)
|
||||
|
||||
is SettingsDestination.Licenses -> LicensesScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
onBackClick = backClick,
|
||||
)
|
||||
|
||||
is SettingsDestination.Settings -> {
|
||||
@ -149,7 +153,7 @@ fun SettingsScreen(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.settings),
|
||||
onBackClick = onBackClick,
|
||||
onBackClick = backClick,
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@ -157,61 +161,27 @@ fun SettingsScreen(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
AnimatedVisibility(visible = showBatteryButton) {
|
||||
Card(
|
||||
onClick = {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Default.BatteryAlert,
|
||||
text = stringResource(R.string.battery_optimization_notification),
|
||||
primaryAction = {
|
||||
context.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
})
|
||||
showBatteryButton =
|
||||
!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) ->
|
||||
ListItem(
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { navController.navigate(destination) },
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(titleDescIcon.first),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
stringResource(titleDescIcon.second),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
},
|
||||
headlineContent = stringResource(titleDescIcon.first),
|
||||
supportingContent = stringResource(titleDescIcon.second),
|
||||
leadingContent = { Icon(titleDescIcon.third, null) }
|
||||
)
|
||||
}
|
||||
@ -220,4 +190,4 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,18 @@
|
||||
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.border
|
||||
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.verticalScroll
|
||||
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.Language
|
||||
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.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.destination.SettingsDestination
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.util.isDebuggable
|
||||
import app.revanced.manager.util.openUrl
|
||||
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
|
||||
fun AboutSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
@ -37,7 +51,10 @@ fun AboutSettingsScreen(
|
||||
onLicensesClick: () -> Unit,
|
||||
) {
|
||||
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(
|
||||
Triple(Icons.Outlined.FavoriteBorder, stringResource(R.string.donate)) {
|
||||
@ -57,17 +74,25 @@ fun AboutSettingsScreen(
|
||||
}),
|
||||
)
|
||||
|
||||
val listItems = listOf(
|
||||
Triple(stringResource(R.string.submit_feedback), stringResource(R.string.submit_feedback_description),
|
||||
val listItems = listOfNotNull(
|
||||
Triple(stringResource(R.string.submit_feedback),
|
||||
stringResource(R.string.submit_feedback_description),
|
||||
third = {
|
||||
context.openUrl("https://github.com/ReVanced/revanced-manager/issues/new/choose")
|
||||
}),
|
||||
Triple(stringResource(R.string.contributors), stringResource(R.string.contributors_description),
|
||||
third = onContributorsClick),
|
||||
Triple(stringResource(R.string.developer_options), stringResource(R.string.developer_options_description),
|
||||
third = { /*TODO*/ }),
|
||||
Triple(stringResource(R.string.opensource_licenses), stringResource(R.string.opensource_licenses_description),
|
||||
third = onLicensesClick)
|
||||
Triple(
|
||||
stringResource(R.string.contributors),
|
||||
stringResource(R.string.contributors_description),
|
||||
third = onContributorsClick
|
||||
),
|
||||
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(
|
||||
@ -82,96 +107,87 @@ fun AboutSettingsScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.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(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Image(painter = icon, contentDescription = null)
|
||||
Text(stringResource(R.string.app_name), style = MaterialTheme.typography.titleLarge)
|
||||
Text( text = stringResource(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")", style = MaterialTheme.typography.bodyMedium)
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
) {
|
||||
filledButton.forEach { (icon, text, onClick) ->
|
||||
FilledTonalButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
Text(
|
||||
stringResource(R.string.app_name),
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
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(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.padding(end = 8.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
) {
|
||||
outlinedButton.forEach { (icon, text, onClick) ->
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
),
|
||||
border = ButtonDefaults.outlinedButtonBorder
|
||||
outlinedButton.forEach { (icon, text, onClick) ->
|
||||
OutlinedButton(
|
||||
onClick = onClick
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.padding(end = 8.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
.padding(16.dp)
|
||||
OutlinedCard(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.about_revanced_manager),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.revanced_manager_description),
|
||||
@ -179,18 +195,17 @@ fun AboutSettingsScreen(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
listItems.forEach { (title, description, onClick) ->
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
.clickable { onClick() },
|
||||
headlineContent = { Text(title, style = MaterialTheme.typography.titleLarge) },
|
||||
supportingContent = { Text(description, style = MaterialTheme.typography.bodyMedium,color = MaterialTheme.colorScheme.outline) }
|
||||
)
|
||||
Column {
|
||||
listItems.forEach { (title, description, onClick) ->
|
||||
SettingsListItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() },
|
||||
headlineContent = title,
|
||||
supportingContent = description
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ import androidx.compose.material.icons.outlined.Http
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
@ -36,6 +35,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
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.viewmodel.AdvancedSettingsViewModel
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
@ -70,7 +70,7 @@ fun AdvancedSettingsScreen(
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
val apiUrl by vm.apiUrl.getAsState()
|
||||
val apiUrl by vm.prefs.api.getAsState()
|
||||
var showApiUrlDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showApiUrlDialog) {
|
||||
@ -79,9 +79,9 @@ fun AdvancedSettingsScreen(
|
||||
it?.let(vm::setApiUrl)
|
||||
}
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.api_url)) },
|
||||
supportingContent = { Text(apiUrl) },
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.api_url),
|
||||
supportingContent = apiUrl,
|
||||
modifier = Modifier.clickable {
|
||||
showApiUrlDialog = true
|
||||
}
|
||||
@ -89,42 +89,48 @@ fun AdvancedSettingsScreen(
|
||||
|
||||
GroupHeader(stringResource(R.string.patcher))
|
||||
BooleanItem(
|
||||
preference = vm.allowExperimental,
|
||||
preference = vm.prefs.allowExperimental,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.experimental_patches,
|
||||
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))
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.patch_bundles_redownload)) },
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.patch_bundles_redownload),
|
||||
modifier = Modifier.clickable {
|
||||
vm.redownloadBundles()
|
||||
}
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.patch_bundles_reset)) },
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.patch_bundles_reset),
|
||||
modifier = Modifier.clickable {
|
||||
vm.resetBundles()
|
||||
}
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.device))
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.device_model)) },
|
||||
supportingContent = { Text(Build.MODEL) }
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.device_model),
|
||||
supportingContent = Build.MODEL
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.device_android_version)) },
|
||||
supportingContent = { Text(Build.VERSION.RELEASE) }
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.device_android_version),
|
||||
supportingContent = Build.VERSION.RELEASE
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.device_architectures)) },
|
||||
supportingContent = { Text(Build.SUPPORTED_ABIS.joinToString(", ")) }
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.device_architectures),
|
||||
supportingContent = Build.SUPPORTED_ABIS.joinToString(", ")
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.device_memory_limit)) },
|
||||
supportingContent = { Text(memoryLimit) }
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.device_memory_limit),
|
||||
supportingContent = memoryLimit
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +1,39 @@
|
||||
package app.revanced.manager.ui.screen.settings
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.border
|
||||
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.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowDropDown
|
||||
import androidx.compose.material.icons.outlined.ArrowDropUp
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
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.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
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.times
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.network.dto.ReVancedContributor
|
||||
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.viewmodel.ContributorViewModel
|
||||
import coil.compose.AsyncImage
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ContributorScreen(
|
||||
@ -45,92 +49,148 @@ fun ContributorScreen(
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(paddingValues)
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = if (repositories.isNullOrEmpty()) Arrangement.Center else Arrangement.spacedBy(
|
||||
24.dp
|
||||
)
|
||||
) {
|
||||
if(repositories.isEmpty()) {
|
||||
LoadingIndicator()
|
||||
}
|
||||
repositories.forEach {
|
||||
ExpandableListCard(
|
||||
title = it.name,
|
||||
contributors = it.contributors
|
||||
)
|
||||
}
|
||||
repositories?.let { repositories ->
|
||||
if (repositories.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.no_contributors_found),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
} 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
|
||||
fun ExpandableListCard(
|
||||
fun ContributorsCard(
|
||||
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(
|
||||
shape = RoundedCornerShape(30.dp),
|
||||
elevation = CardDefaults.outlinedCardElevation(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
),
|
||||
colors = CardDefaults.outlinedCardColors(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer)
|
||||
) {
|
||||
Column() {
|
||||
Row() {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = processHeadlineText(title),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
if (contributors.isNotEmpty()) {
|
||||
ArrowButton(
|
||||
expanded = expanded,
|
||||
onClick = { expanded = !expanded }
|
||||
)
|
||||
}
|
||||
},
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = processHeadlineText(title),
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Medium)
|
||||
)
|
||||
Text(
|
||||
text = "(${(pagerState.currentPage + 1)}/${pagerState.pageCount})",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold)
|
||||
)
|
||||
}
|
||||
if (expanded) {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(8.dp),
|
||||
) {
|
||||
contributors.forEach {
|
||||
AsyncImage(
|
||||
model = it.avatarUrl,
|
||||
contentDescription = it.avatarUrl,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.size(45.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
userScrollEnabled = true,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) { page ->
|
||||
BoxWithConstraints {
|
||||
val spaceBetween = 16.dp
|
||||
val maxWidth = this.maxWidth
|
||||
val itemSize = (maxWidth - (itemsPerRow - 1) * spaceBetween) / itemsPerRow
|
||||
val itemSpacing = (maxWidth - itemSize * 6) / (itemsPerRow - 1)
|
||||
FlowRow(
|
||||
maxItemsInEachRow = itemsPerRow,
|
||||
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
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 {
|
||||
return "Revanced " + repositoryName.replace("revanced/revanced-", "")
|
||||
return "ReVanced " + repositoryName.replace("revanced/revanced-", "")
|
||||
.replace("-", " ")
|
||||
.split(" ")
|
||||
.map { if (it.length > 3) it else it.uppercase() }
|
||||
.joinToString(" ")
|
||||
.split(" ").joinToString(" ") { if (it.length > 3) it else it.uppercase() }
|
||||
.replaceFirstChar { it.uppercase() }
|
||||
}
|
@ -8,12 +8,11 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -23,6 +22,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
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.viewmodel.DownloadsViewModel
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
@ -66,12 +66,20 @@ fun DownloadsSettingsScreen(
|
||||
|
||||
GroupHeader(stringResource(R.string.downloaded_apps))
|
||||
|
||||
downloadedApps.forEach {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { viewModel.toggleItem(it) },
|
||||
headlineContent = { Text(it.packageName) },
|
||||
supportingContent = { Text(it.version) },
|
||||
tonalElevation = if (viewModel.selection.contains(it)) 8.dp else 0.dp
|
||||
downloadedApps.forEach { app ->
|
||||
val selected = app in viewModel.selection
|
||||
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { viewModel.toggleItem(app) },
|
||||
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.viewmodel.SettingsViewModel
|
||||
import org.koin.compose.koinInject
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -58,12 +59,12 @@ fun GeneralSettingsScreen(
|
||||
GroupHeader(stringResource(R.string.appearance))
|
||||
|
||||
val theme by prefs.theme.getAsState()
|
||||
ListItem(
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { showThemePicker = true },
|
||||
headlineContent = { Text(stringResource(R.string.theme)) },
|
||||
supportingContent = { Text(stringResource(R.string.theme_description)) },
|
||||
headlineContent = stringResource(R.string.theme),
|
||||
supportingContent = stringResource(R.string.theme_description),
|
||||
trailingContent = {
|
||||
Button(
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
showThemePicker = true
|
||||
}
|
||||
|
@ -6,7 +6,10 @@ import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
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.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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 kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -53,8 +58,10 @@ fun ImportExportSettingsScreen(
|
||||
it?.let(vm::exportKeystore)
|
||||
}
|
||||
|
||||
val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
|
||||
|
||||
vm.selectionAction?.let { action ->
|
||||
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
|
||||
if (uri == null) {
|
||||
vm.clearSelectionAction()
|
||||
@ -64,7 +71,7 @@ fun ImportExportSettingsScreen(
|
||||
}
|
||||
|
||||
if (vm.selectedBundle == null) {
|
||||
BundleSelector(sources) {
|
||||
BundleSelector(patchBundles) {
|
||||
if (it == null) {
|
||||
vm.clearSelectionAction()
|
||||
} else {
|
||||
@ -137,21 +144,120 @@ fun ImportExportSettingsScreen(
|
||||
headline = R.string.backup_patches_selection,
|
||||
description = R.string.backup_patches_selection_description
|
||||
)
|
||||
// TODO: allow resetting selection for specific bundle or package name.
|
||||
GroupItem(
|
||||
onClick = vm::resetSelection,
|
||||
headline = R.string.clear_patches_selection,
|
||||
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
|
||||
private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes description: Int) =
|
||||
ListItem(
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { onClick() },
|
||||
headlineContent = { Text(stringResource(headline)) },
|
||||
supportingContent = { Text(stringResource(description)) }
|
||||
headlineContent = stringResource(headline),
|
||||
supportingContent = stringResource(description)
|
||||
)
|
||||
|
||||
@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
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.Row
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Update
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.NotificationCard
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -69,59 +60,22 @@ fun UpdatesSettingsScreen(
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
UpdateNotification(
|
||||
onClick = onUpdateClick
|
||||
NotificationCard(
|
||||
text = stringResource(R.string.update_notification),
|
||||
icon = Icons.Default.Update,
|
||||
primaryAction = onUpdateClick
|
||||
)
|
||||
|
||||
listItems.forEach { (title, description, onClick) ->
|
||||
ListItem(
|
||||
SettingsListItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
.padding(horizontal = 8.dp)
|
||||
.clickable { onClick() },
|
||||
headlineContent = {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
headlineContent = title,
|
||||
supportingContent = description
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
class AdvancedSettingsViewModel(
|
||||
prefs: PreferencesManager,
|
||||
val prefs: PreferencesManager,
|
||||
private val app: Application,
|
||||
private val patchBundleRepository: PatchBundleRepository
|
||||
) : ViewModel() {
|
||||
val apiUrl = prefs.api
|
||||
val allowExperimental = prefs.allowExperimental
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
@ -11,13 +14,14 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ContributorViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() {
|
||||
val repositories = mutableStateListOf<ReVancedGitRepository>()
|
||||
var repositories: List<ReVancedGitRepository>? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) { reVancedAPI.getContributors().getOrNull() }?.let(
|
||||
repositories::addAll
|
||||
)
|
||||
repositories = withContext(Dispatchers.IO) {
|
||||
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.PatchBundleRepository
|
||||
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.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
@ -38,10 +39,11 @@ class ImportExportViewModel(
|
||||
private val app: Application,
|
||||
private val keystoreManager: KeystoreManager,
|
||||
private val selectionRepository: PatchSelectionRepository,
|
||||
private val optionsRepository: PatchOptionsRepository,
|
||||
patchBundleRepository: PatchBundleRepository
|
||||
) : ViewModel() {
|
||||
private val contentResolver = app.contentResolver
|
||||
val sources = patchBundleRepository.sources
|
||||
val patchBundles = patchBundleRepository.sources
|
||||
var selectedBundle by mutableStateOf<PatchBundleSource?>(null)
|
||||
private set
|
||||
var selectionAction by mutableStateOf<SelectionAction?>(null)
|
||||
@ -49,6 +51,20 @@ class ImportExportViewModel(
|
||||
private var keystoreImportPath by mutableStateOf<Path?>(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 {
|
||||
val path = withContext(Dispatchers.IO) {
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
@ -30,7 +31,7 @@ import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class AppInfoViewModel(
|
||||
class InstalledAppInfoViewModel(
|
||||
val installedApp: InstalledApp
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val app: Application by inject()
|
||||
@ -83,8 +84,10 @@ class AppInfoViewModel(
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||
val extraStatus = intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
|
||||
val extraStatusMessage = intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
val extraStatus =
|
||||
intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
|
||||
val extraStatusMessage =
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
|
||||
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||
viewModelScope.launch {
|
||||
@ -113,9 +116,11 @@ class AppInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
app.registerReceiver(
|
||||
ContextCompat.registerReceiver(
|
||||
app,
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.map
|
||||
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.InstalledApp
|
||||
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.worker.WorkerRepository
|
||||
import app.revanced.manager.patcher.worker.PatcherProgressManager
|
||||
@ -56,7 +56,6 @@ import java.util.logging.LogRecord
|
||||
class InstallerViewModel(
|
||||
private val input: Destination.Installer
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val keystoreManager: KeystoreManager by inject()
|
||||
private val app: Application by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
private val pm: PM by inject()
|
||||
@ -71,8 +70,6 @@ class InstallerViewModel(
|
||||
}
|
||||
|
||||
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 installedApp: InstalledApp? = null
|
||||
@ -97,11 +94,13 @@ class InstallerViewModel(
|
||||
|
||||
val (selectedApp, patches, options) = input
|
||||
|
||||
_progress = MutableStateFlow(PatcherProgressManager.generateSteps(
|
||||
app,
|
||||
patches.flatMap { (_, selected) -> selected },
|
||||
selectedApp
|
||||
).toImmutableList())
|
||||
_progress = MutableStateFlow(
|
||||
PatcherProgressManager.generateSteps(
|
||||
app,
|
||||
patches.flatMap { (_, selected) -> selected },
|
||||
selectedApp
|
||||
).toImmutableList()
|
||||
)
|
||||
|
||||
patcherWorkerId =
|
||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
@ -140,7 +139,7 @@ class InstallerViewModel(
|
||||
installedPackageName =
|
||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
||||
viewModelScope.launch {
|
||||
installedAppRepository.add(
|
||||
installedAppRepository.addOrUpdate(
|
||||
installedPackageName!!,
|
||||
packageName,
|
||||
input.selectedApp.version,
|
||||
@ -160,10 +159,10 @@ class InstallerViewModel(
|
||||
}
|
||||
|
||||
init {
|
||||
app.registerReceiver(installBroadcastReceiver, IntentFilter().apply {
|
||||
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
|
||||
addAction(InstallService.APP_INSTALL_ACTION)
|
||||
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
||||
})
|
||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
|
||||
fun exportLogs(context: Context) {
|
||||
@ -186,62 +185,47 @@ class InstallerViewModel(
|
||||
is SelectedApp.Local -> {
|
||||
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 -> {}
|
||||
}
|
||||
|
||||
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 {
|
||||
uri?.let {
|
||||
if (signApk()) {
|
||||
withContext(Dispatchers.IO) {
|
||||
app.contentResolver.openOutputStream(it)
|
||||
.use { stream -> Files.copy(signedFile.toPath(), stream) }
|
||||
}
|
||||
app.toast(app.getString(R.string.export_app_success))
|
||||
withContext(Dispatchers.IO) {
|
||||
app.contentResolver.openOutputStream(it)
|
||||
.use { stream -> Files.copy(outputFile.toPath(), stream) }
|
||||
}
|
||||
app.toast(app.getString(R.string.save_apk_success))
|
||||
}
|
||||
}
|
||||
|
||||
fun install(installType: InstallType) = viewModelScope.launch {
|
||||
isInstalling = true
|
||||
try {
|
||||
if (!signApk()) return@launch
|
||||
|
||||
when (installType) {
|
||||
InstallType.DEFAULT -> { pm.installApp(listOf(signedFile)) }
|
||||
InstallType.DEFAULT -> {
|
||||
pm.installApp(listOf(outputFile))
|
||||
}
|
||||
|
||||
InstallType.ROOT -> { installAsRoot() }
|
||||
InstallType.ROOT -> {
|
||||
installAsRoot()
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
@ -254,7 +238,7 @@ class InstallerViewModel(
|
||||
private suspend fun installAsRoot() {
|
||||
try {
|
||||
val label = with(pm) {
|
||||
getPackageInfo(signedFile)?.label()
|
||||
getPackageInfo(outputFile)?.label()
|
||||
?: throw Exception("Failed to load application info")
|
||||
}
|
||||
|
||||
@ -270,7 +254,7 @@ class InstallerViewModel(
|
||||
|
||||
installedApp?.let { installedAppRepository.delete(it) }
|
||||
|
||||
installedAppRepository.add(
|
||||
installedAppRepository.addOrUpdate(
|
||||
packageName,
|
||||
packageName,
|
||||
input.selectedApp.version,
|
||||
@ -286,7 +270,8 @@ class InstallerViewModel(
|
||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||
try {
|
||||
rootInstaller.uninstall(packageName)
|
||||
} catch (_: Exception) { }
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,77 @@
|
||||
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.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.manager.KeystoreManager
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
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.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class MainViewModel(
|
||||
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
|
||||
) : 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 {
|
||||
prefs.showAutoUpdatesDialog.update(false)
|
||||
prefs.firstLaunch.update(false)
|
||||
|
||||
prefs.managerAutoUpdates.update(manager)
|
||||
|
||||
if (manager) checkForManagerUpdates()
|
||||
|
||||
if (patches) {
|
||||
with(patchBundleRepository) {
|
||||
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 androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
@ -17,69 +17,45 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||
import androidx.lifecycle.viewmodel.compose.saveable
|
||||
import app.revanced.manager.R
|
||||
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.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.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.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import kotlinx.collections.immutable.*
|
||||
import java.util.Optional
|
||||
|
||||
@Stable
|
||||
@OptIn(SavedStateHandleSaveableApi::class)
|
||||
class PatchesSelectorViewModel(
|
||||
val input: Destination.PatchesSelector
|
||||
) : ViewModel(), KoinComponent {
|
||||
class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||
private val app: Application = get()
|
||||
private val selectionRepository: PatchSelectionRepository = get()
|
||||
private val savedStateHandle: SavedStateHandle = 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 selectionWarningEnabled by mutableStateOf(true)
|
||||
private set
|
||||
|
||||
val allowExperimental = get<PreferencesManager>().allowExperimental
|
||||
val bundlesFlow = get<PatchBundleRepository>().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(
|
||||
input.selectedApp.packageName,
|
||||
input.selectedApp.version
|
||||
) -> supported
|
||||
|
||||
else -> unsupported
|
||||
}
|
||||
|
||||
targetList.add(it)
|
||||
}
|
||||
|
||||
BundleInfo(source.name, source.uid, bundle.patches, supported, unsupported, universal)
|
||||
}
|
||||
}
|
||||
val allowExperimental = get<PreferencesManager>().allowExperimental.getBlocking()
|
||||
val bundlesFlow =
|
||||
get<PatchBundleRepository>().bundleInfoFlow(packageName, input.app.version)
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
@ -88,63 +64,28 @@ class PatchesSelectorViewModel(
|
||||
return@launch
|
||||
}
|
||||
|
||||
val experimental = allowExperimental.get()
|
||||
fun BundleInfo.hasDefaultPatches(): Boolean {
|
||||
return if (experimental) {
|
||||
all.asSequence()
|
||||
} else {
|
||||
sequence {
|
||||
yieldAll(supported)
|
||||
yieldAll(universal)
|
||||
}
|
||||
}.any { it.include }
|
||||
}
|
||||
fun BundleInfo.hasDefaultPatches() = patchSequence(allowExperimental).any { it.include }
|
||||
|
||||
// Don't show the warning if there are no default patches.
|
||||
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 customPatchesSelection: PersistentPatchesSelection? by savedStateHandle.saveable(
|
||||
key = "selection",
|
||||
stateSaver = patchesSaver,
|
||||
) {
|
||||
mutableStateOf(input.currentSelection?.toPersistentPatchesSelection())
|
||||
}
|
||||
|
||||
private val explicitPatchesSelection: SnapshotExplicitPatchesSelection by savedStateHandle.saveable(
|
||||
saver = explicitPatchesSelectionSaver,
|
||||
init = ::mutableStateMapOf
|
||||
)
|
||||
|
||||
private val patchOptions: SnapshotOptions by savedStateHandle.saveable(
|
||||
private val patchOptions: PersistentOptions by savedStateHandle.saveable(
|
||||
saver = optionsSaver,
|
||||
init = ::mutableStateMapOf
|
||||
)
|
||||
|
||||
private val selectors by derivedStateOf<Array<Selector>> {
|
||||
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
|
||||
})
|
||||
}
|
||||
)
|
||||
) {
|
||||
// Convert Options to PersistentOptions
|
||||
input.options.mapValuesTo(mutableStateMapOf()) { (_, allPatches) ->
|
||||
allPatches.mapValues { (_, options) -> options.toPersistentMap() }.toPersistentMap()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,38 +95,39 @@ class PatchesSelectorViewModel(
|
||||
|
||||
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 suspend fun loadPreviousSelection() {
|
||||
val selection = (input.patchesSelection ?: selectionRepository.getSelection(
|
||||
packageName
|
||||
)).mapValues { (_, value) -> value.toSet() }
|
||||
private suspend fun generateDefaultSelection(): PersistentPatchesSelection {
|
||||
val bundles = bundlesFlow.first()
|
||||
val generatedSelection =
|
||||
bundles.toPatchSelection(allowExperimental) { _, patch -> patch.include }
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
previousPatchesSelection.putAll(selection)
|
||||
return generatedSelection.toPersistentPatchesSelection()
|
||||
}
|
||||
|
||||
fun selectionIsValid(bundles: List<BundleInfo>) = bundles.any { bundle ->
|
||||
bundle.patchSequence(allowExperimental).any { patch ->
|
||||
isSelected(bundle.uid, patch)
|
||||
}
|
||||
}
|
||||
|
||||
fun switchBaseSelectionMode() = viewModelScope.launch {
|
||||
baseSelectionMode = if (baseSelectionMode == BaseSelectionMode.DEFAULT) {
|
||||
BaseSelectionMode.PREVIOUS
|
||||
} 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 isSelected(bundle: Int, patch: PatchInfo) = customPatchesSelection?.let { selection ->
|
||||
selection[bundle]?.contains(patch.name) ?: false
|
||||
} ?: patch.include
|
||||
|
||||
fun togglePatch(bundle: Int, patch: PatchInfo) = viewModelScope.launch {
|
||||
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) {
|
||||
@ -207,46 +149,34 @@ class PatchesSelectorViewModel(
|
||||
|
||||
fun reset() {
|
||||
patchOptions.clear()
|
||||
baseSelectionMode = BaseSelectionMode.DEFAULT
|
||||
explicitPatchesSelection.clear()
|
||||
customPatchesSelection = null
|
||||
hasModifiedSelection = false
|
||||
app.toast(app.getString(R.string.patch_selection_reset_toast))
|
||||
}
|
||||
|
||||
suspend fun getSelection(): PatchesSelection {
|
||||
val bundles = bundlesFlow.first()
|
||||
val removeUnsupported = !allowExperimental.get()
|
||||
fun getCustomSelection(): PatchesSelection? {
|
||||
// Convert persistent collections to standard hash collections because persistent collections are not parcelable.
|
||||
|
||||
return bundles.associate { bundle ->
|
||||
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
|
||||
}
|
||||
return customPatchesSelection?.mapValues { (_, v) -> v.toSet() }
|
||||
}
|
||||
|
||||
suspend fun saveSelection(selection: PatchesSelection) =
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
when {
|
||||
hasModifiedSelection -> selectionRepository.updateSelection(packageName, selection)
|
||||
baseSelectionMode == BaseSelectionMode.DEFAULT -> selectionRepository.clearSelection(
|
||||
packageName
|
||||
)
|
||||
fun getOptions(): Options {
|
||||
// Convert the collection for the same reasons as in getCustomSelection()
|
||||
|
||||
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 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) {
|
||||
@ -260,7 +190,7 @@ class PatchesSelectorViewModel(
|
||||
|
||||
fun openUnsupportedDialog(unsupportedPatches: List<PatchInfo>) {
|
||||
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_UNSUPPORTED = 4 // 2^2
|
||||
|
||||
private fun <K, K2, V> SnapshotStateMap<K, SnapshotStateMap<K2, V>>.getOrCreate(key: K) =
|
||||
getOrPut(key, ::mutableStateMapOf)
|
||||
|
||||
private val optionsSaver: Saver<SnapshotOptions, Options> = snapshotStateMapSaver(
|
||||
private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver(
|
||||
// Patch name -> Options
|
||||
valueSaver = snapshotStateMapSaver(
|
||||
valueSaver = persistentMapSaver(
|
||||
// Option key -> Option value
|
||||
valueSaver = snapshotStateMapSaver()
|
||||
valueSaver = persistentMapSaver()
|
||||
)
|
||||
)
|
||||
|
||||
private val explicitPatchesSelectionSaver: Saver<SnapshotExplicitPatchesSelection, ExplicitPatchesSelection> =
|
||||
snapshotStateMapSaver(valueSaver = snapshotStateMapSaver())
|
||||
private val patchesSaver: Saver<PersistentPatchesSelection?, Nullable<PatchesSelection>> =
|
||||
nullableSaver(persistentMapSaver(valueSaver = persistentSetSaver()))
|
||||
}
|
||||
|
||||
/**
|
||||
* An enum for controlling the behavior of the selector.
|
||||
*/
|
||||
enum class BaseSelectionMode {
|
||||
/**
|
||||
* 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>
|
||||
data class Params(
|
||||
val app: SelectedApp,
|
||||
val currentSelection: PatchesSelection?,
|
||||
val options: Options,
|
||||
)
|
||||
}
|
||||
|
||||
private typealias Selector = (Int, PatchInfo) -> Boolean?
|
||||
private typealias ExplicitPatchesSelection = Map<Int, Map<String, Boolean>>
|
||||
// Versions of other types, but utilizing persistent/observable collection types.
|
||||
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 typealias SnapshotOptions = SnapshotStateMap<Int, SnapshotStateMap<String, SnapshotStateMap<String, Any?>>>
|
||||
private typealias SnapshotExplicitPatchesSelection = SnapshotStateMap<Int, SnapshotStateMap<String, Boolean>>
|
||||
private fun PatchesSelection.toPersistentPatchesSelection(): PersistentPatchesSelection =
|
||||
mapValues { (_, v) -> v.toPersistentSet() }.toPersistentMap()
|
@ -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 ->
|
||||
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 {
|
||||
|
@ -98,7 +98,18 @@ class PM(
|
||||
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()
|
||||
|
||||
|
@ -2,6 +2,12 @@ package app.revanced.manager.util
|
||||
|
||||
import android.content.Context
|
||||
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.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
@ -11,17 +17,28 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import app.revanced.manager.R
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
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
|
||||
|
||||
typealias PatchesSelection = Map<Int, Set<String>>
|
||||
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) {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
|
||||
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 block The code to execute.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
||||
try {
|
||||
block()
|
||||
} catch (error: Exception) {
|
||||
context.toast(
|
||||
context.getString(
|
||||
toastMsg,
|
||||
error.simpleMessage()
|
||||
// You can only toast on the main thread.
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
context.toast(
|
||||
context.getString(
|
||||
toastMsg,
|
||||
error.simpleMessage()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Log.e(tag, logMsg, error)
|
||||
}
|
||||
}
|
||||
@ -97,4 +119,50 @@ suspend fun <T> Flow<Iterable<T>>.collectEach(block: suspend (T) -> Unit) {
|
||||
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_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_bundle">Import patch bundle</string>
|
||||
<string name="bundle_patches">Bundle patches</string>
|
||||
@ -23,7 +25,19 @@
|
||||
|
||||
<string name="bundle_missing">Missing</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_description">Periodically connect to update providers to check for updates.</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="theme">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_description">Allow patching incompatible patches with experimental versions, something may break</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="clear_patches_selection">Clear 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_description">Prefer split apks instead of full apks</string>
|
||||
<string name="prefer_universal">Prefer universal apks</string>
|
||||
@ -143,8 +166,6 @@
|
||||
<string name="unsupported_app">Unsupported app</string>
|
||||
<string name="unsupported_patches">Unsupported 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="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>
|
||||
@ -152,6 +173,7 @@
|
||||
<string name="supported">Supported</string>
|
||||
<string name="universal">Universal</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="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>
|
||||
@ -188,6 +210,9 @@
|
||||
<string name="downloadable_versions">Downloadable versions</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_menu_description">More options</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="uninstall_app_fail">Failed to uninstall app: %s</string>
|
||||
<string name="open_app">Open</string>
|
||||
<string name="export_app">Export</string>
|
||||
<string name="export_app_success">Apk exported</string>
|
||||
<string name="save_apk">Save APK</string>
|
||||
<string name="save_apk_success">APK Saved</string>
|
||||
<string name="sign_fail">Failed to sign Apk: %s</string>
|
||||
<string name="save_logs">Save logs</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_saving">Saving</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="step_completed">completed</string>
|
||||
@ -230,6 +256,7 @@
|
||||
|
||||
<string name="more">More</string>
|
||||
<string name="continue_">Continue</string>
|
||||
<string name="dismiss">Dismiss</string>
|
||||
<string name="permanent_dismiss">Do not show this again</string>
|
||||
<string name="donate">Donate</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="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="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_channel">Update channel</string>
|
||||
<string name="update_channel_description">Stable</string>
|
||||
@ -257,7 +290,7 @@
|
||||
<string name="changelog_loading">Loading changelog</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="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="downloading_manager_update">Downloading update…</string>
|
||||
<string name="download_manager_failed">Failed to download update: %s</string>
|
||||
@ -265,4 +298,21 @@
|
||||
<string name="save">Save</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="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>
|
@ -1,24 +1,25 @@
|
||||
[versions]
|
||||
ktx = "1.10.1"
|
||||
ktx = "1.12.0"
|
||||
viewmodel-lifecycle = "2.6.2"
|
||||
splash-screen = "1.0.1"
|
||||
compose-activity = "1.7.2"
|
||||
compose-activity = "1.8.0"
|
||||
paging = "3.2.1"
|
||||
preferences-datastore = "1.0.0"
|
||||
work-runtime = "2.8.1"
|
||||
compose-bom = "2023.06.01"
|
||||
compose-bom = "2023.10.00"
|
||||
accompanist = "0.30.1"
|
||||
serialization = "1.6.0"
|
||||
collection = "0.3.5"
|
||||
room-version = "2.5.2"
|
||||
revanced-patcher = "16.0.1"
|
||||
revanced-library = "1.1.1"
|
||||
revanced-patcher = "19.1.0"
|
||||
revanced-library = "1.4.0"
|
||||
koin-version = "3.4.3"
|
||||
koin-version-compose = "3.4.6"
|
||||
reimagined-navigation = "1.4.0"
|
||||
reimagined-navigation = "1.5.0"
|
||||
ktor = "2.3.3"
|
||||
markdown = "0.5.0"
|
||||
androidGradlePlugin = "8.1.1"
|
||||
markdown-renderer = "0.8.0"
|
||||
fading-edges = "1.0.4"
|
||||
androidGradlePlugin = "8.1.2"
|
||||
kotlinGradlePlugin = "1.9.10"
|
||||
devToolsGradlePlugin = "1.9.10-1.0.13"
|
||||
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-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
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" }
|
||||
|
||||
# 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" }
|
||||
|
||||
# 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-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
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=591855b517fc635b9e04de1d05d5e76ada3f89f5fc76f87978d1b245b4f69225
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
14
gradlew
vendored
14
gradlew
vendored
@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# 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 ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# 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" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
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.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
@ -1,59 +1,19 @@
|
||||
pluginManagement {
|
||||
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()
|
||||
google()
|
||||
mavenCentral()
|
||||
maven("https://jitpack.io")
|
||||
githubPackages("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
githubPackages("https://maven.pkg.github.com/revanced/revanced-library")
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
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()
|
||||
mavenCentral()
|
||||
maven("https://jitpack.io")
|
||||
githubPackages("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
githubPackages("https://maven.pkg.github.com/revanced/revanced-library")
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
rootProject.name = "ReVanced Manager"
|
||||
|
Loading…
x
Reference in New Issue
Block a user